diff --git a/deployments/artifacts/FlowYieldVaultsRequests.json b/deployments/artifacts/FlowYieldVaultsRequests.json index 6dd99bc..60b0e10 100644 --- a/deployments/artifacts/FlowYieldVaultsRequests.json +++ b/deployments/artifacts/FlowYieldVaultsRequests.json @@ -15,10 +15,6 @@ ], "stateMutability": "nonpayable" }, - { - "type": "receive", - "stateMutability": "payable" - }, { "type": "function", "name": "DEFAULT_MINIMUM_BALANCE", @@ -947,8 +943,13 @@ }, { "type": "function", - "name": "pendingRequestIds", + "name": "pendingRequestIdsByUser", "inputs": [ + { + "name": "", + "type": "address", + "internalType": "address" + }, { "name": "", "type": "uint256", @@ -966,7 +967,7 @@ }, { "type": "function", - "name": "pendingRequestIdsByUser", + "name": "pendingUserBalances", "inputs": [ { "name": "", @@ -975,8 +976,8 @@ }, { "name": "", - "type": "uint256", - "internalType": "uint256" + "type": "address", + "internalType": "address" } ], "outputs": [ @@ -990,27 +991,26 @@ }, { "type": "function", - "name": "pendingUserBalances", + "name": "recoverTokens", "inputs": [ { - "name": "", + "name": "to", "type": "address", "internalType": "address" }, { - "name": "", + "name": "tokenAddress", "type": "address", "internalType": "address" - } - ], - "outputs": [ + }, { - "name": "", + "name": "amount", "type": "uint256", "internalType": "uint256" } ], - "stateMutability": "view" + "outputs": [], + "stateMutability": "nonpayable" }, { "type": "function", @@ -1186,6 +1186,25 @@ "outputs": [], "stateMutability": "nonpayable" }, + { + "type": "function", + "name": "totalAccountedBalance", + "inputs": [ + { + "name": "", + "type": "address", + "internalType": "address" + } + ], + "outputs": [ + { + "name": "", + "type": "uint256", + "internalType": "uint256" + } + ], + "stateMutability": "view" + }, { "type": "function", "name": "transferOwnership", @@ -1860,6 +1879,31 @@ ], "anonymous": false }, + { + "type": "event", + "name": "TokensRecovered", + "inputs": [ + { + "name": "to", + "type": "address", + "indexed": true, + "internalType": "address" + }, + { + "name": "tokenAddress", + "type": "address", + "indexed": true, + "internalType": "address" + }, + { + "name": "amount", + "type": "uint256", + "indexed": false, + "internalType": "uint256" + } + ], + "anonymous": false + }, { "type": "event", "name": "Unpaused", @@ -1990,6 +2034,11 @@ "name": "EmptyAddressArray", "inputs": [] }, + { + "type": "error", + "name": "EmptyRequestsQueue", + "inputs": [] + }, { "type": "error", "name": "EmptyStrategyIdentifier", @@ -2021,6 +2070,22 @@ } ] }, + { + "type": "error", + "name": "InsufficientRecoveryAmount", + "inputs": [ + { + "name": "available", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "requested", + "type": "uint256", + "internalType": "uint256" + } + ] + }, { "type": "error", "name": "InvalidCOAAddress", @@ -2031,6 +2096,16 @@ "name": "InvalidMinimumBalance", "inputs": [] }, + { + "type": "error", + "name": "InvalidRecoveryTokenAddress", + "inputs": [] + }, + { + "type": "error", + "name": "InvalidRecoveryUserAddress", + "inputs": [] + }, { "type": "error", "name": "InvalidRefundAmount", @@ -2148,6 +2223,33 @@ "name": "RequestNotFound", "inputs": [] }, + { + "type": "error", + "name": "RequestNotInQueue", + "inputs": [ + { + "name": "requestId", + "type": "uint256", + "internalType": "uint256" + } + ] + }, + { + "type": "error", + "name": "RequestProcessOutOfOrder", + "inputs": [ + { + "name": "expectedId", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "processedId", + "type": "uint256", + "internalType": "uint256" + } + ] + }, { "type": "error", "name": "SafeERC20FailedOperation", diff --git a/solidity/deployments/artifacts/FlowYieldVaultsRequests.json b/solidity/deployments/artifacts/FlowYieldVaultsRequests.json index 6dd99bc..60b0e10 100644 --- a/solidity/deployments/artifacts/FlowYieldVaultsRequests.json +++ b/solidity/deployments/artifacts/FlowYieldVaultsRequests.json @@ -15,10 +15,6 @@ ], "stateMutability": "nonpayable" }, - { - "type": "receive", - "stateMutability": "payable" - }, { "type": "function", "name": "DEFAULT_MINIMUM_BALANCE", @@ -947,8 +943,13 @@ }, { "type": "function", - "name": "pendingRequestIds", + "name": "pendingRequestIdsByUser", "inputs": [ + { + "name": "", + "type": "address", + "internalType": "address" + }, { "name": "", "type": "uint256", @@ -966,7 +967,7 @@ }, { "type": "function", - "name": "pendingRequestIdsByUser", + "name": "pendingUserBalances", "inputs": [ { "name": "", @@ -975,8 +976,8 @@ }, { "name": "", - "type": "uint256", - "internalType": "uint256" + "type": "address", + "internalType": "address" } ], "outputs": [ @@ -990,27 +991,26 @@ }, { "type": "function", - "name": "pendingUserBalances", + "name": "recoverTokens", "inputs": [ { - "name": "", + "name": "to", "type": "address", "internalType": "address" }, { - "name": "", + "name": "tokenAddress", "type": "address", "internalType": "address" - } - ], - "outputs": [ + }, { - "name": "", + "name": "amount", "type": "uint256", "internalType": "uint256" } ], - "stateMutability": "view" + "outputs": [], + "stateMutability": "nonpayable" }, { "type": "function", @@ -1186,6 +1186,25 @@ "outputs": [], "stateMutability": "nonpayable" }, + { + "type": "function", + "name": "totalAccountedBalance", + "inputs": [ + { + "name": "", + "type": "address", + "internalType": "address" + } + ], + "outputs": [ + { + "name": "", + "type": "uint256", + "internalType": "uint256" + } + ], + "stateMutability": "view" + }, { "type": "function", "name": "transferOwnership", @@ -1860,6 +1879,31 @@ ], "anonymous": false }, + { + "type": "event", + "name": "TokensRecovered", + "inputs": [ + { + "name": "to", + "type": "address", + "indexed": true, + "internalType": "address" + }, + { + "name": "tokenAddress", + "type": "address", + "indexed": true, + "internalType": "address" + }, + { + "name": "amount", + "type": "uint256", + "indexed": false, + "internalType": "uint256" + } + ], + "anonymous": false + }, { "type": "event", "name": "Unpaused", @@ -1990,6 +2034,11 @@ "name": "EmptyAddressArray", "inputs": [] }, + { + "type": "error", + "name": "EmptyRequestsQueue", + "inputs": [] + }, { "type": "error", "name": "EmptyStrategyIdentifier", @@ -2021,6 +2070,22 @@ } ] }, + { + "type": "error", + "name": "InsufficientRecoveryAmount", + "inputs": [ + { + "name": "available", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "requested", + "type": "uint256", + "internalType": "uint256" + } + ] + }, { "type": "error", "name": "InvalidCOAAddress", @@ -2031,6 +2096,16 @@ "name": "InvalidMinimumBalance", "inputs": [] }, + { + "type": "error", + "name": "InvalidRecoveryTokenAddress", + "inputs": [] + }, + { + "type": "error", + "name": "InvalidRecoveryUserAddress", + "inputs": [] + }, { "type": "error", "name": "InvalidRefundAmount", @@ -2148,6 +2223,33 @@ "name": "RequestNotFound", "inputs": [] }, + { + "type": "error", + "name": "RequestNotInQueue", + "inputs": [ + { + "name": "requestId", + "type": "uint256", + "internalType": "uint256" + } + ] + }, + { + "type": "error", + "name": "RequestProcessOutOfOrder", + "inputs": [ + { + "name": "expectedId", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "processedId", + "type": "uint256", + "internalType": "uint256" + } + ] + }, { "type": "error", "name": "SafeERC20FailedOperation", diff --git a/solidity/src/FlowYieldVaultsRequests.sol b/solidity/src/FlowYieldVaultsRequests.sol index c62a5bb..7012bb1 100644 --- a/solidity/src/FlowYieldVaultsRequests.sol +++ b/solidity/src/FlowYieldVaultsRequests.sol @@ -185,16 +185,20 @@ contract FlowYieldVaultsRequests is ReentrancyGuard, Ownable2Step { /// @notice All requests indexed by request ID mapping(uint256 => Request) public requests; - /// @notice Array of pending request IDs awaiting processing (FIFO order) - uint256[] public pendingRequestIds; - - /// @notice Index of request ID in global pending array (for O(1) lookup) - mapping(uint256 => uint256) private _requestIndexInGlobalArray; - /// @notice Index of yieldVaultId in user's yieldVaultsByUser array (for O(1) removal) /// @dev Internal visibility allows test helpers to properly initialize state mapping(address => mapping(uint64 => uint256)) internal _yieldVaultIndexInUserArray; + /// @notice Mapping of queued request IDs awaiting processing (FIFO order) + mapping(uint256 => uint256) private _requestsQueue; + + /// @notice Pointer to the current head in _requestsQueue. Denotes the next request to be processed + uint256 private _requestsQueueHead = 1; + + /// @notice Pointer to the current tail in _requestsQueue. Points to the next available + /// slot — i.e., one past the last enqueued request. + uint256 private _requestsQueueTail = 1; + // ============================================ // Errors // ============================================ @@ -309,6 +313,15 @@ contract FlowYieldVaultsRequests is ReentrancyGuard, Ownable2Step { /// @notice The requested recovery amount exceeds the available excess amount error InsufficientRecoveryAmount(uint256 available, uint256 requested); + /// @notice Invalid dequeue operation on an empty requests queue + error EmptyRequestsQueue(); + + /// @notice Processed request does not match the head of requestsQueue + error RequestProcessOutOfOrder(uint256 expectedId, uint256 processedId); + + /// @notice Request is not included in requestsQueue + error RequestNotInQueue(uint256 requestId); + // ============================================ // Events // ============================================ @@ -901,7 +914,8 @@ contract FlowYieldVaultsRequests is ReentrancyGuard, Ownable2Step { if (userPendingRequestCount[request.user] > 0) { userPendingRequestCount[request.user]--; } - _removePendingRequest(requestId); + _removeUserPendingRequest(requestId); + _dropQueuedRequest(requestId); // === REFUND HANDLING (pull pattern) === // For CREATE/DEPOSIT requests, move funds from pendingUserBalances to claimableRefunds @@ -971,6 +985,10 @@ contract FlowYieldVaultsRequests is ReentrancyGuard, Ownable2Step { * @notice Processes a batch of PENDING requests. * @dev For successful requests, marks them as PROCESSING. * For rejected requests, marks them as FAILED. + * Requests are classified as successful/rejected based on validation + * logic that is performed on Cadence side, and not on the authorized + * COA's discretion. + * Both arrays containing the request IDs must be in ascending FIFO queue order. * Single-request processing is supported by passing one request id in * successfulRequestIds and an empty rejectedRequestIds array. * @param successfulRequestIds The request ids to start processing (PENDING -> PROCESSING) @@ -980,7 +998,50 @@ contract FlowYieldVaultsRequests is ReentrancyGuard, Ownable2Step { uint256[] calldata successfulRequestIds, uint256[] calldata rejectedRequestIds ) external onlyAuthorizedCOA nonReentrant { + uint256 j = 0; + uint256 k = 0; + // Validate that the given IDs for successful/rejected requests, + // are according to the FIFO queue order. + uint256 totalRequests = successfulRequestIds.length + rejectedRequestIds.length; + for (uint256 i = 0; i < totalRequests; i++) { + uint256 requestId; + // If reqId is 0, it means we went over the boundaries of + // _requestsQueue. + uint256 reqId = _requestsQueue[_requestsQueueHead+i]; + if (j < successfulRequestIds.length) { + requestId = successfulRequestIds[j]; + if (reqId == 0) revert RequestNotInQueue(requestId); + if (reqId == requestId) { + j++; + continue; + } + } + + if (k < rejectedRequestIds.length) { + requestId = rejectedRequestIds[k]; + if (reqId == 0) revert RequestNotInQueue(requestId); + if (reqId == requestId) { + k++; + continue; + } + } + + // === VALIDATION === + Request storage request = requests[reqId]; + if (request.status != RequestStatus.PENDING) + revert InvalidRequestState(); + + // requestId currently holds the last-assigned candidate + // (from rejectedIds if both branches ran). + // Prefer the successful candidate for a clearer error. + uint256 candidateReqId = (j < successfulRequestIds.length) + ? successfulRequestIds[j] + : requestId; + revert RequestProcessOutOfOrder(reqId, candidateReqId); + } + // First the rejected request IDs are dropped, so successful + // request IDs are contiguous at the head before dequeue // === REJECTED REQUESTS === _dropRequestsInternal(rejectedRequestIds); @@ -1176,12 +1237,21 @@ contract FlowYieldVaultsRequests is ReentrancyGuard, Ownable2Step { /// @notice Gets the count of pending requests /// @return Number of pending requests function getPendingRequestCount() external view returns (uint256) { - return pendingRequestIds.length; + return _requestsQueueLength(); } /// @notice Gets all pending request IDs /// @return Array of pending request IDs function getPendingRequestIds() external view returns (uint256[] memory) { + uint256[] memory pendingRequestIds = new uint256[](_requestsQueueLength()); + uint256 arrayIndex = 0; + for (uint256 i = _requestsQueueHead; i < _requestsQueueTail;) { + pendingRequestIds[arrayIndex] = _requestsQueue[i]; + unchecked { + ++arrayIndex; + ++i; + } + } return pendingRequestIds; } @@ -1220,7 +1290,7 @@ contract FlowYieldVaultsRequests is ReentrancyGuard, Ownable2Step { string[] memory strategyIdentifiers ) { - if (startIndex >= pendingRequestIds.length) { + if (startIndex >= _requestsQueueLength()) { return ( new uint256[](0), new address[](0), @@ -1236,7 +1306,7 @@ contract FlowYieldVaultsRequests is ReentrancyGuard, Ownable2Step { ); } - uint256 remaining = pendingRequestIds.length - startIndex; + uint256 remaining = _requestsQueueLength() - startIndex; uint256 size = count == 0 ? remaining : (count < remaining ? count : remaining); @@ -1253,8 +1323,8 @@ contract FlowYieldVaultsRequests is ReentrancyGuard, Ownable2Step { vaultIdentifiers = new string[](size); strategyIdentifiers = new string[](size); - for (uint256 i = 0; i < size; ) { - Request memory req = requests[pendingRequestIds[startIndex + i]]; + for (uint256 i = 0; i < size;) { + Request memory req = requests[_requestsQueue[_requestsQueueHead + startIndex + i]]; ids[i] = req.id; users[i] = req.user; requestTypes[i] = uint8(req.requestType); @@ -1516,7 +1586,8 @@ contract FlowYieldVaultsRequests is ReentrancyGuard, Ownable2Step { } // Remove from pending queues (both global and user-specific) - _removePendingRequest(requestId); + _removeUserPendingRequest(requestId); + _dropQueuedRequest(requestId); emit RequestProcessed( requestId, @@ -1587,6 +1658,9 @@ contract FlowYieldVaultsRequests is ReentrancyGuard, Ownable2Step { if (request.status != RequestStatus.PENDING) revert InvalidRequestState(); + uint256 reqId = _dequeueRequest(); + if (reqId != requestId) revert RequestProcessOutOfOrder(reqId, requestId); + // === TRANSITION TO PROCESSING === // This prevents cancellation and ensures atomicity with completeProcessing request.status = RequestStatus.PROCESSING; @@ -1643,7 +1717,7 @@ contract FlowYieldVaultsRequests is ReentrancyGuard, Ownable2Step { if (userPendingRequestCount[request.user] > 0) { userPendingRequestCount[request.user]--; } - _removePendingRequest(requestId); + _removeUserPendingRequest(requestId); emit RequestProcessed( requestId, @@ -1908,9 +1982,8 @@ contract FlowYieldVaultsRequests is ReentrancyGuard, Ownable2Step { strategyIdentifier: strategyIdentifier }); - // Add to global pending queue with index tracking for O(1) lookup - _requestIndexInGlobalArray[requestId] = pendingRequestIds.length; - pendingRequestIds.push(requestId); + // Add to the global FIFO queue in O(1) by writing at the tail pointer + _enqueueRequest(requestId); userPendingRequestCount[msg.sender]++; // Add to user's pending array with index tracking for O(1) removal @@ -1946,40 +2019,16 @@ contract FlowYieldVaultsRequests is ReentrancyGuard, Ownable2Step { } /** - * @dev Removes a request from all pending queues while preserving request history. - * Uses two different removal strategies: - * - Global array: Shift elements to maintain FIFO order (O(n) but necessary for fair processing) - * - User array: Swap-and-pop for O(1) removal (order doesn't affect processing) + * @dev Removes a request from the user pending requests mapping while preserving request history. + * Uses the following removal strategy: + * - Swap-and-pop for O(1) removal (order doesn't affect processing) * * The request data remains in the `requests` mapping for historical queries; - * this function only removes it from the pending queues. - * @param requestId The request ID to remove from pending queues. + * This function only removes it from the user pending requests mapping. + * @param requestId The request ID to remove from the user pending requests mapping. */ - function _removePendingRequest(uint256 requestId) internal { - // === GLOBAL PENDING ARRAY REMOVAL === - // Uses O(1) lookup + O(n) shift to maintain FIFO order - // FIFO order is critical for DeFi fairness - requests must be processed in submission order - uint256 indexInGlobal = _requestIndexInGlobalArray[requestId]; - uint256 globalLength = pendingRequestIds.length; - - // Safety check: verify element exists at expected index - if (globalLength > 0 && indexInGlobal < globalLength && pendingRequestIds[indexInGlobal] == requestId) { - // Shift all subsequent elements left to maintain FIFO order - for (uint256 j = indexInGlobal; j < globalLength - 1; ) { - pendingRequestIds[j] = pendingRequestIds[j + 1]; - // Update index mapping for each shifted element - _requestIndexInGlobalArray[pendingRequestIds[j]] = j; - unchecked { - ++j; - } - } - // Remove the last element (now duplicated or the one to remove) - pendingRequestIds.pop(); - // Clean up index mapping - delete _requestIndexInGlobalArray[requestId]; - } - - // === USER PENDING ARRAY REMOVAL === + function _removeUserPendingRequest(uint256 requestId) internal { + // === USER PENDING REQUESTS ARRAY REMOVAL === // Uses swap-and-pop for O(1) removal (order doesn't affect FIFO processing) address user = requests[requestId].user; uint256[] storage userPendingIds = pendingRequestIdsByUser[user]; @@ -2001,4 +2050,70 @@ contract FlowYieldVaultsRequests is ReentrancyGuard, Ownable2Step { delete _requestIndexInUserArray[requestId]; } } + + /** + * @dev Enqueues a request in the requestsQueue and shifts the queue's tail pointer. + * + * @param requestId The request ID to enqueue in the pending requests queue. + */ + function _enqueueRequest(uint256 requestId) internal { + _requestsQueue[_requestsQueueTail] = requestId; + _requestsQueueTail += 1; + } + + /** + * @dev Dequeues the head of requestsQueue and shifts the queue's head pointer. + * + * @return The request ID that was dequeued. + */ + function _dequeueRequest() internal returns (uint256) { + if (_requestsQueueLength() == 0) revert EmptyRequestsQueue(); + + uint256 requestId = _requestsQueue[_requestsQueueHead]; + + delete _requestsQueue[_requestsQueueHead]; + _requestsQueueHead += 1; + + return requestId; + } + + /** + * @dev Drops a request from the requestsQueue. + * O(n) operation — scans from the head until the request is found, then + * shifts all subsequent queue entries one slot left to maintain FIFO order. + * + * @param requestId The request ID to remove from the pending requests queue. + */ + function _dropQueuedRequest(uint256 requestId) internal { + bool requestFound = false; + for (uint256 i = _requestsQueueHead; i < _requestsQueueTail;) { + if (_requestsQueue[i] == requestId) { + requestFound = true; + } + + // Once found, shift later requests left and delete the duplicated tail slot + if (requestFound && (i + 1 < _requestsQueueTail)) { + _requestsQueue[i] = _requestsQueue[i + 1]; + } else if (requestFound) { + delete _requestsQueue[i]; + } + + unchecked { + ++i; + } + } + + // Decrement the queue tail only if the given requestId was found + if (!requestFound) revert RequestNotFound(); + _requestsQueueTail -= 1; + } + + /** + * @dev Counts the total number of pending requests in the requestsQueue. + * + * @return The current requestsQueue length. + */ + function _requestsQueueLength() internal view returns (uint256) { + return _requestsQueueTail - _requestsQueueHead; + } } diff --git a/solidity/test/FlowYieldVaultsRequests.t.sol b/solidity/test/FlowYieldVaultsRequests.t.sol index 50eaff0..9056b76 100644 --- a/solidity/test/FlowYieldVaultsRequests.t.sol +++ b/solidity/test/FlowYieldVaultsRequests.t.sol @@ -365,15 +365,30 @@ contract FlowYieldVaultsRequestsTest is Test { assertEq(uint8(req.status), uint8(FlowYieldVaultsRequests.RequestStatus.PROCESSING)); } - function test_StartProcessingBatch_RevertNotPending() public { + function test_StartProcessingBatch_RevertRequestNotInQueueWhenQueueEmpty() public { + vm.startPrank(coa); + vm.expectRevert(abi.encodeWithSelector( + FlowYieldVaultsRequests.RequestNotInQueue.selector, + 1 + )); + _startProcessingBatch(1); + vm.stopPrank(); + } + + function test_StartProcessingBatch_RevertRequestNotInQueueWhenQueueNonEmpty() public { vm.prank(user); uint256 reqId = c.createYieldVault{value: 1 ether}(NATIVE_FLOW, 1 ether, VAULT_ID, STRATEGY_ID); vm.startPrank(coa); - _startProcessingBatch(reqId); + uint256[] memory successfulRequestIds = new uint256[](5); + successfulRequestIds[0] = reqId; + successfulRequestIds[1] = reqId+2; - vm.expectRevert(FlowYieldVaultsRequests.InvalidRequestState.selector); - _startProcessingBatch(reqId); + vm.expectRevert(abi.encodeWithSelector( + FlowYieldVaultsRequests.RequestNotInQueue.selector, + reqId+2 + )); + c.startProcessingBatch(successfulRequestIds, new uint256[](0)); vm.stopPrank(); } @@ -386,6 +401,281 @@ contract FlowYieldVaultsRequestsTest is Test { _startProcessingBatch(reqId); } + function test_StartProcessingBatch_WithMoreRequestIdsThanQueued() public { + vm.startPrank(user); + uint256 req1 = c.createYieldVault{value: 1 ether}(NATIVE_FLOW, 1 ether, VAULT_ID, STRATEGY_ID); + uint256 req2 = c.createYieldVault{value: 2 ether}(NATIVE_FLOW, 2 ether, VAULT_ID, STRATEGY_ID); + uint256 req3 = c.createYieldVault{value: 3 ether}(NATIVE_FLOW, 3 ether, VAULT_ID, STRATEGY_ID); + vm.stopPrank(); + + vm.prank(coa); + uint256[] memory successfulRequestIds = new uint256[](5); + successfulRequestIds[0] = req1; + successfulRequestIds[1] = req2; + successfulRequestIds[2] = req3; + successfulRequestIds[3] = req3+5; + successfulRequestIds[4] = req3+10; + + vm.expectRevert(abi.encodeWithSelector( + FlowYieldVaultsRequests.RequestNotInQueue.selector, + req3+5 + )); + c.startProcessingBatch(successfulRequestIds, new uint256[](0)); + + (uint256[] memory requestIds, , , , , , , , , , ) = c.getPendingRequestsUnpacked(0, 0); + assertEq(requestIds.length, 3); + + for (uint256 i = 0; i < requestIds.length; i++) { + FlowYieldVaultsRequests.Request memory req = c.getRequest(successfulRequestIds[i]); + assertEq(uint8(req.status), uint8(FlowYieldVaultsRequests.RequestStatus.PENDING)); + } + } + + function test_StartProcessingBatch_RevertRequestProcessOutOfOrder() public { + vm.startPrank(user); + uint256 req1 = c.createYieldVault{value: 1 ether}(NATIVE_FLOW, 1 ether, VAULT_ID, STRATEGY_ID); + uint256 req2 = c.createYieldVault{value: 2 ether}(NATIVE_FLOW, 2 ether, VAULT_ID, STRATEGY_ID); + vm.stopPrank(); + + vm.startPrank(coa); + vm.expectRevert(abi.encodeWithSelector( + FlowYieldVaultsRequests.RequestProcessOutOfOrder.selector, + req1, // the expected requestId, in the queue head + req2 // the provided requestId + )); + _startProcessingBatch(req2); + vm.stopPrank(); + } + + function test_StartProcessingBatch_MultipleSuccessfulRequests() public { + vm.startPrank(user); + uint256 req1 = c.createYieldVault{value: 1 ether}(NATIVE_FLOW, 1 ether, VAULT_ID, STRATEGY_ID); + uint256 req2 = c.createYieldVault{value: 2 ether}(NATIVE_FLOW, 2 ether, VAULT_ID, STRATEGY_ID); + uint256 req3 = c.createYieldVault{value: 3 ether}(NATIVE_FLOW, 3 ether, VAULT_ID, STRATEGY_ID); + uint256 req4 = c.createYieldVault{value: 4 ether}(NATIVE_FLOW, 4 ether, VAULT_ID, STRATEGY_ID); + uint256 req5 = c.createYieldVault{value: 5 ether}(NATIVE_FLOW, 5 ether, VAULT_ID, STRATEGY_ID); + uint256 req6 = c.createYieldVault{value: 6 ether}(NATIVE_FLOW, 6 ether, VAULT_ID, STRATEGY_ID); + vm.stopPrank(); + + vm.startPrank(coa); + // First 3 successful, transition to PROCESSING + uint256[] memory successfulRequestIds = new uint256[](3); + successfulRequestIds[0] = req1; + successfulRequestIds[1] = req2; + successfulRequestIds[2] = req3; + c.startProcessingBatch(successfulRequestIds, new uint256[](0)); + + (uint256[] memory requestIds, , , , , , , , , , ) = c.getPendingRequestsUnpacked(0, 0); + assertEq(requestIds.length, 3); + + for (uint256 i = 0; i < successfulRequestIds.length; i++) { + FlowYieldVaultsRequests.Request memory req = c.getRequest(successfulRequestIds[i]); + assertEq(uint8(req.status), uint8(FlowYieldVaultsRequests.RequestStatus.PROCESSING)); + } + + // Remaining 3 successful, transition to PROCESSING + successfulRequestIds[0] = req4; + successfulRequestIds[1] = req5; + successfulRequestIds[2] = req6; + c.startProcessingBatch(successfulRequestIds, new uint256[](0)); + + (requestIds, , , , , , , , , , ) = c.getPendingRequestsUnpacked(0, 0); + assertEq(requestIds.length, 0); + + for (uint256 i = 0; i < successfulRequestIds.length; i++) { + FlowYieldVaultsRequests.Request memory req = c.getRequest(successfulRequestIds[i]); + assertEq(uint8(req.status), uint8(FlowYieldVaultsRequests.RequestStatus.PROCESSING)); + } + vm.stopPrank(); + } + + function test_StartProcessingBatch_WithUserCancellationRace() public { + vm.startPrank(user); + uint256 req1 = c.createYieldVault{value: 1 ether}(NATIVE_FLOW, 1 ether, VAULT_ID, STRATEGY_ID); + uint256 req2 = c.createYieldVault{value: 2 ether}(NATIVE_FLOW, 2 ether, VAULT_ID, STRATEGY_ID); + uint256 req3 = c.createYieldVault{value: 3 ether}(NATIVE_FLOW, 3 ether, VAULT_ID, STRATEGY_ID); + c.cancelRequest(req2); + vm.stopPrank(); + + vm.startPrank(coa); + // First and third requests, transition to PROCESSING + uint256[] memory successfulRequestIds = new uint256[](2); + successfulRequestIds[0] = req1; + successfulRequestIds[1] = req3; + // Second request, transition to FAILED + uint256[] memory rejectedRequestIds = new uint256[](1); + rejectedRequestIds[0] = req2; + + vm.expectRevert(abi.encodeWithSelector( + FlowYieldVaultsRequests.RequestNotInQueue.selector, + req2 + )); + c.startProcessingBatch(successfulRequestIds, rejectedRequestIds); + + (uint256[] memory requestIds, , , , , , , , , , ) = c.getPendingRequestsUnpacked(0, 0); + assertEq(requestIds.length, 2); + + for (uint256 i = 0; i < successfulRequestIds.length; i++) { + FlowYieldVaultsRequests.Request memory req = c.getRequest(successfulRequestIds[i]); + assertEq(uint8(req.status), uint8(FlowYieldVaultsRequests.RequestStatus.PENDING)); + } + + // Try again, without the user-cancelled request + c.startProcessingBatch(successfulRequestIds, new uint256[](0)); + + (requestIds, , , , , , , , , , ) = c.getPendingRequestsUnpacked(0, 0); + assertEq(requestIds.length, 0); + + for (uint256 i = 0; i < successfulRequestIds.length; i++) { + FlowYieldVaultsRequests.Request memory req = c.getRequest(successfulRequestIds[i]); + assertEq(uint8(req.status), uint8(FlowYieldVaultsRequests.RequestStatus.PROCESSING)); + } + } + + function test_StartProcessingBatch_MultipleSuccessfulAndRejectRequests() public { + vm.startPrank(user); + uint256 req1 = c.createYieldVault{value: 1 ether}(NATIVE_FLOW, 1 ether, VAULT_ID, STRATEGY_ID); + uint256 req2 = c.createYieldVault{value: 2 ether}(NATIVE_FLOW, 2 ether, VAULT_ID, STRATEGY_ID); + uint256 req3 = c.createYieldVault{value: 3 ether}(NATIVE_FLOW, 3 ether, VAULT_ID, STRATEGY_ID); + uint256 req4 = c.createYieldVault{value: 4 ether}(NATIVE_FLOW, 4 ether, VAULT_ID, STRATEGY_ID); + uint256 req5 = c.createYieldVault{value: 5 ether}(NATIVE_FLOW, 5 ether, VAULT_ID, STRATEGY_ID); + vm.stopPrank(); + + vm.prank(coa); + // 2 successful, 3 rejected + uint256[] memory successfulRequestIds = new uint256[](2); + successfulRequestIds[0] = req2; + successfulRequestIds[1] = req4; + uint256[] memory rejectedRequestIds = new uint256[](3); + rejectedRequestIds[0] = req1; + rejectedRequestIds[1] = req3; + rejectedRequestIds[2] = req5; + c.startProcessingBatch(successfulRequestIds, rejectedRequestIds); + + (uint256[] memory requestIds, , , , , , , , , , ) = c.getPendingRequestsUnpacked(0, 0); + assertEq(requestIds.length, 0); + + for (uint256 i = 0; i < successfulRequestIds.length; i++) { + FlowYieldVaultsRequests.Request memory req = c.getRequest(successfulRequestIds[i]); + assertEq(uint8(req.status), uint8(FlowYieldVaultsRequests.RequestStatus.PROCESSING)); + } + + for (uint256 i = 0; i < rejectedRequestIds.length; i++) { + FlowYieldVaultsRequests.Request memory req = c.getRequest(rejectedRequestIds[i]); + assertEq(uint8(req.status), uint8(FlowYieldVaultsRequests.RequestStatus.FAILED)); + } + } + + function test_StartProcessingBatch_MultipleRejectedRequests() public { + vm.startPrank(user); + uint256 req1 = c.createYieldVault{value: 1 ether}(NATIVE_FLOW, 1 ether, VAULT_ID, STRATEGY_ID); + uint256 req2 = c.createYieldVault{value: 2 ether}(NATIVE_FLOW, 2 ether, VAULT_ID, STRATEGY_ID); + uint256 req3 = c.createYieldVault{value: 3 ether}(NATIVE_FLOW, 3 ether, VAULT_ID, STRATEGY_ID); + uint256 req4 = c.createYieldVault{value: 4 ether}(NATIVE_FLOW, 4 ether, VAULT_ID, STRATEGY_ID); + uint256 req5 = c.createYieldVault{value: 5 ether}(NATIVE_FLOW, 5 ether, VAULT_ID, STRATEGY_ID); + vm.stopPrank(); + + vm.prank(coa); + // All 5 rejected, transition to FAILED + uint256[] memory rejectedRequestIds = new uint256[](5); + rejectedRequestIds[0] = req1; + rejectedRequestIds[1] = req2; + rejectedRequestIds[2] = req3; + rejectedRequestIds[3] = req4; + rejectedRequestIds[4] = req5; + c.startProcessingBatch(new uint256[](0), rejectedRequestIds); + + (uint256[] memory requestIds, , , , , , , , , , ) = c.getPendingRequestsUnpacked(0, 0); + assertEq(requestIds.length, 0); + + for (uint256 i = 0; i < rejectedRequestIds.length; i++) { + FlowYieldVaultsRequests.Request memory req = c.getRequest(rejectedRequestIds[i]); + assertEq(uint8(req.status), uint8(FlowYieldVaultsRequests.RequestStatus.FAILED)); + } + } + + function test_StartProcessingBatch_MultipleSuccessfulRequestsRevertRequestProcessOutOfOrder() public { + vm.startPrank(user); + uint256 req1 = c.createYieldVault{value: 1 ether}(NATIVE_FLOW, 1 ether, VAULT_ID, STRATEGY_ID); + uint256 req2 = c.createYieldVault{value: 2 ether}(NATIVE_FLOW, 2 ether, VAULT_ID, STRATEGY_ID); + uint256 req3 = c.createYieldVault{value: 3 ether}(NATIVE_FLOW, 3 ether, VAULT_ID, STRATEGY_ID); + uint256 req4 = c.createYieldVault{value: 4 ether}(NATIVE_FLOW, 4 ether, VAULT_ID, STRATEGY_ID); + uint256 req5 = c.createYieldVault{value: 5 ether}(NATIVE_FLOW, 5 ether, VAULT_ID, STRATEGY_ID); + vm.stopPrank(); + + vm.prank(coa); + // 2 successful but out-of-order, 3 rejected + uint256[] memory successfulRequestIds = new uint256[](2); + // These are out-of-order + successfulRequestIds[0] = req4; + successfulRequestIds[1] = req2; + uint256[] memory rejectedRequestIds = new uint256[](3); + rejectedRequestIds[0] = req1; + rejectedRequestIds[1] = req3; + rejectedRequestIds[2] = req5; + + vm.expectRevert(abi.encodeWithSelector( + FlowYieldVaultsRequests.RequestProcessOutOfOrder.selector, + req2, // queue[head+1] — the current queue position that doesn't match either input array + req4 // the first provided requestId, from successfulRequestIds + )); + c.startProcessingBatch(successfulRequestIds, rejectedRequestIds); + + (uint256[] memory requestIds, , , , , , , , , , ) = c.getPendingRequestsUnpacked(0, 0); + assertEq(requestIds.length, 5); + + for (uint256 i = 0; i < successfulRequestIds.length; i++) { + FlowYieldVaultsRequests.Request memory req = c.getRequest(successfulRequestIds[i]); + assertEq(uint8(req.status), uint8(FlowYieldVaultsRequests.RequestStatus.PENDING)); + } + + for (uint256 i = 0; i < rejectedRequestIds.length; i++) { + FlowYieldVaultsRequests.Request memory req = c.getRequest(rejectedRequestIds[i]); + assertEq(uint8(req.status), uint8(FlowYieldVaultsRequests.RequestStatus.PENDING)); + } + } + + function test_StartProcessingBatch_MultipleRejectedRequestsOutOfOrder() public { + vm.startPrank(user); + uint256 req1 = c.createYieldVault{value: 1 ether}(NATIVE_FLOW, 1 ether, VAULT_ID, STRATEGY_ID); + uint256 req2 = c.createYieldVault{value: 2 ether}(NATIVE_FLOW, 2 ether, VAULT_ID, STRATEGY_ID); + uint256 req3 = c.createYieldVault{value: 3 ether}(NATIVE_FLOW, 3 ether, VAULT_ID, STRATEGY_ID); + uint256 req4 = c.createYieldVault{value: 4 ether}(NATIVE_FLOW, 4 ether, VAULT_ID, STRATEGY_ID); + uint256 req5 = c.createYieldVault{value: 5 ether}(NATIVE_FLOW, 5 ether, VAULT_ID, STRATEGY_ID); + vm.stopPrank(); + + vm.prank(coa); + // 2 successful, 3 rejected but out-of-order + uint256[] memory successfulRequestIds = new uint256[](2); + successfulRequestIds[0] = req4; + successfulRequestIds[1] = req5; + uint256[] memory rejectedRequestIds = new uint256[](3); + // These are out-of-order + rejectedRequestIds[0] = req3; + rejectedRequestIds[1] = req1; + rejectedRequestIds[2] = req2; + + vm.expectRevert(abi.encodeWithSelector( + FlowYieldVaultsRequests.RequestProcessOutOfOrder.selector, + req1, // the expected requestId, in the queue head + req4 // the first provided requestId, from successfulRequestIds + )); + c.startProcessingBatch(successfulRequestIds, rejectedRequestIds); + + (uint256[] memory requestIds, , , , , , , , , , ) = c.getPendingRequestsUnpacked(0, 0); + assertEq(requestIds.length, 5); + + for (uint256 i = 0; i < successfulRequestIds.length; i++) { + FlowYieldVaultsRequests.Request memory req = c.getRequest(successfulRequestIds[i]); + assertEq(uint8(req.status), uint8(FlowYieldVaultsRequests.RequestStatus.PENDING)); + } + + for (uint256 i = 0; i < rejectedRequestIds.length; i++) { + FlowYieldVaultsRequests.Request memory req = c.getRequest(rejectedRequestIds[i]); + assertEq(uint8(req.status), uint8(FlowYieldVaultsRequests.RequestStatus.PENDING)); + } + } + function test_CompleteProcessing_Success() public { vm.prank(user); uint256 reqId = c.createYieldVault{value: 1 ether}(NATIVE_FLOW, 1 ether, VAULT_ID, STRATEGY_ID); @@ -1151,17 +1441,16 @@ contract FlowYieldVaultsRequestsTest is Test { vm.prank(user); uint256 req5 = c.createYieldVault{value: 1 ether}(NATIVE_FLOW, 1 ether, VAULT_ID, STRATEGY_ID); - // Process middle request (req3) vm.startPrank(coa); - _startProcessingBatch(req3); - _completeProcessingNoRefund(req3, true, 200, "Created"); + _startProcessingBatch(req1); + _completeProcessingNoRefund(req1, true, 200, "Created"); vm.stopPrank(); - // Verify FIFO order is maintained: [req1, req2, req4, req5] + // Verify FIFO order is maintained: [req2, req3, req4, req5] (uint256[] memory ids, , , , , , , , , , ) = c.getPendingRequestsUnpacked(0, 0); assertEq(ids.length, 4, "Should have 4 pending requests"); - assertEq(ids[0], req1, "First should be req1"); - assertEq(ids[1], req2, "Second should be req2"); + assertEq(ids[0], req2, "First should be req2"); + assertEq(ids[1], req3, "Second should be req3"); assertEq(ids[2], req4, "Third should be req4"); assertEq(ids[3], req5, "Fourth should be req5"); } @@ -1192,11 +1481,8 @@ contract FlowYieldVaultsRequestsTest is Test { uint256 req3 = c.createYieldVault{value: 1 ether}(NATIVE_FLOW, 1 ether, VAULT_ID, STRATEGY_ID); vm.stopPrank(); - // Remove last element - vm.startPrank(coa); - _startProcessingBatch(req3); - _completeProcessingNoRefund(req3, true, 100, "Created"); - vm.stopPrank(); + // Remove last queue element, by cancelling the last request + c.cancelRequest(req3); (uint256[] memory ids, , , , , , , , , , ) = c.getPendingRequestsUnpacked(0, 0); assertEq(ids.length, 2); @@ -1234,35 +1520,34 @@ contract FlowYieldVaultsRequestsTest is Test { uint256 req4 = c.createYieldVault{value: 1 ether}(NATIVE_FLOW, 1 ether, VAULT_ID, STRATEGY_ID); vm.stopPrank(); - // Process out of order: req2, req4, req1, req3 vm.startPrank(coa); - _startProcessingBatch(req2); - _completeProcessingNoRefund(req2, true, 100, "Created"); + _startProcessingBatch(req1); + _completeProcessingNoRefund(req1, true, 100, "Created"); - // After removing req2: [req1, req3, req4] + // After removing req1: [req2, req3, req4] (uint256[] memory ids1, , , , , , , , , , ) = c.getPendingRequestsUnpacked(0, 0); - assertEq(ids1[0], req1); + assertEq(ids1[0], req2); assertEq(ids1[1], req3); assertEq(ids1[2], req4); - _startProcessingBatch(req4); - _completeProcessingNoRefund(req4, true, 101, "Created"); + _startProcessingBatch(req2); + _completeProcessingNoRefund(req2, true, 101, "Created"); - // After removing req4: [req1, req3] + // After removing req2: [req3, req4] (uint256[] memory ids2, , , , , , , , , , ) = c.getPendingRequestsUnpacked(0, 0); - assertEq(ids2[0], req1); - assertEq(ids2[1], req3); + assertEq(ids2[0], req3); + assertEq(ids2[1], req4); - _startProcessingBatch(req1); - _completeProcessingNoRefund(req1, true, 102, "Created"); + _startProcessingBatch(req3); + _completeProcessingNoRefund(req3, true, 102, "Created"); - // After removing req1: [req3] + // After removing req3: [req4] (uint256[] memory ids3, , , , , , , , , , ) = c.getPendingRequestsUnpacked(0, 0); assertEq(ids3.length, 1); - assertEq(ids3[0], req3); + assertEq(ids3[0], req4); - _startProcessingBatch(req3); - _completeProcessingNoRefund(req3, true, 103, "Created"); + _startProcessingBatch(req4); + _completeProcessingNoRefund(req4, true, 103, "Created"); vm.stopPrank(); assertEq(c.getPendingRequestCount(), 0); @@ -1509,20 +1794,34 @@ contract FlowYieldVaultsRequestsTest is Test { uint256 req3 = c.createYieldVault{value: 3 ether}(NATIVE_FLOW, 3 ether, VAULT_ID, STRATEGY_ID); vm.stopPrank(); - // Process req2 + // Process req1 vm.startPrank(coa); - _startProcessingBatch(req2); - _completeProcessingNoRefund(req2, true, 100, "Created"); + _startProcessingBatch(req1); + _completeProcessingNoRefund(req1, true, 100, "Created"); vm.stopPrank(); - // User should now have req1 and req3 - (uint256[] memory ids, , , , uint256[] memory amounts, , , , , , , uint256[] memory pendingBalances, ) = c.getPendingRequestsByUserUnpacked(user); + // User should now have req2 and req3 + ( + uint256[] memory ids, + , + , + , + , + , + , + , + , + , + , + uint256[] memory pendingBalances, + ) = c.getPendingRequestsByUserUnpacked(user); assertEq(ids.length, 2, "User should have 2 remaining requests"); // Note: Order in user array may change due to swap-and-pop optimization - assertTrue(ids[0] == req1 || ids[0] == req3, "Should contain req1 or req3"); - assertTrue(ids[1] == req1 || ids[1] == req3, "Should contain req1 or req3"); + assertTrue(ids[0] == req2 || ids[0] == req3, "Should contain req2 or req3"); + assertTrue(ids[1] == req2 || ids[1] == req3, "Should contain req2 or req3"); assertTrue(ids[0] != ids[1], "Should be different requests"); - assertEq(pendingBalances[0], 4 ether, "Pending balance should be 4 ether"); + assertEq(pendingBalances[0], 5 ether, "Pending balance should be 5 ether"); + assertEq(pendingBalances[1], 0, "WFLOW pending balance should be 0"); } function test_GetPendingRequestsByUserUnpacked_AfterCancel() public { @@ -1567,10 +1866,10 @@ contract FlowYieldVaultsRequestsTest is Test { vm.prank(user); uint256 u1r3 = c.createYieldVault{value: 1 ether}(NATIVE_FLOW, 1 ether, VAULT_ID, STRATEGY_ID); - // Remove user's middle request (u1r2) + // Process the first queued request vm.startPrank(coa); - _startProcessingBatch(u1r2); - _completeProcessingNoRefund(u1r2, true, 100, "Created"); + _startProcessingBatch(u1r1); + _completeProcessingNoRefund(u1r1, true, 100, "Created"); vm.stopPrank(); // Verify user1's remaining requests @@ -1718,9 +2017,8 @@ contract FlowYieldVaultsRequestsTest is Test { assertEq(c.getPendingRequestCount(), numRequests); - // Process every other request (simulating out-of-order processing) vm.startPrank(coa); - for (uint256 i = 1; i < numRequests; i += 2) { + for (uint256 i = 0; i < numRequests / 2; i++) { _startProcessingBatch(requestIds[i]); _completeProcessingNoRefund(requestIds[i], true, uint64(100 + i), "Created"); } @@ -1733,9 +2031,9 @@ contract FlowYieldVaultsRequestsTest is Test { (uint256[] memory ids, , , , , , , , , , ) = c.getPendingRequestsUnpacked(0, 0); assertEq(ids.length, numRequests / 2); - // Even-indexed original requests should remain in order - for (uint256 i = 0; i < ids.length; i++) { - assertEq(ids[i], requestIds[i * 2], "FIFO order not maintained"); + // Pending requests should remain in order + for (uint256 i = 0; i < ids.length - 1; i++) { + assertEq(ids[i], requestIds[numRequests/2 + i], "FIFO order not maintained"); } } @@ -1757,17 +2055,17 @@ contract FlowYieldVaultsRequestsTest is Test { vm.stopPrank(); } - // Process user[2]'s middle request + // Process the first queued request vm.startPrank(coa); - _startProcessingBatch(userRequestIds[2][1]); - _completeProcessingNoRefund(userRequestIds[2][1], true, 300, "Created"); + _startProcessingBatch(userRequestIds[0][0]); + _completeProcessingNoRefund(userRequestIds[0][0], true, 300, "Created"); vm.stopPrank(); // Verify all other users still have 3 requests for (uint256 i = 0; i < 5; i++) { (uint256[] memory ids, , , , , , , , , , , , ) = c.getPendingRequestsByUserUnpacked(users[i]); - if (i == 2) { - assertEq(ids.length, 2, "User 2 should have 2 requests"); + if (i == 0) { + assertEq(ids.length, 2, "User 0 should have 2 requests"); } else { assertEq(ids.length, 3, "Other users should have 3 requests"); } @@ -2281,4 +2579,55 @@ contract FlowYieldVaultsRequestsTest is Test { vm.expectRevert(FlowYieldVaultsRequests.AmountMustBeGreaterThanZero.selector); c.recoverTokens(user2, address(dai), 0); } + + function test_CancelRequests_RandomOrder() public { + vm.startPrank(user); + uint256 req1 = c.createYieldVault{value: 1 ether}(NATIVE_FLOW, 1 ether, VAULT_ID, STRATEGY_ID); + uint256 req2 = c.createYieldVault{value: 2 ether}(NATIVE_FLOW, 2 ether, VAULT_ID, STRATEGY_ID); + uint256 req3 = c.createYieldVault{value: 3 ether}(NATIVE_FLOW, 3 ether, VAULT_ID, STRATEGY_ID); + uint256 req4 = c.createYieldVault{value: 4 ether}(NATIVE_FLOW, 4 ether, VAULT_ID, STRATEGY_ID); + uint256 req5 = c.createYieldVault{value: 5 ether}(NATIVE_FLOW, 5 ether, VAULT_ID, STRATEGY_ID); + + uint256[] memory requestIDs = c.getPendingRequestIds(); + + assertEq(requestIDs.length, 5); + assertEq(requestIDs[0], req1, "First should be req1"); + assertEq(requestIDs[1], req2, "Second should be req2"); + assertEq(requestIDs[2], req3, "Third should be req3"); + assertEq(requestIDs[3], req4, "Fourth should be req4"); + assertEq(requestIDs[4], req5, "Fifth should be req5"); + + // Cancel last request + c.cancelRequest(req5); + requestIDs = c.getPendingRequestIds(); + + assertEq(requestIDs.length, 4); + assertEq(requestIDs[0], req1, "First should be req1"); + assertEq(requestIDs[1], req2, "Second should be req2"); + assertEq(requestIDs[2], req3, "Third should be req3"); + assertEq(requestIDs[3], req4, "Fourth should be req4"); + + // Cancel first request + c.cancelRequest(req1); + requestIDs = c.getPendingRequestIds(); + + assertEq(requestIDs.length, 3); + assertEq(requestIDs[0], req2, "First should be req2"); + assertEq(requestIDs[1], req3, "Second should be req3"); + assertEq(requestIDs[2], req4, "Third should be req4"); + + // Cancel middle request + c.cancelRequest(req3); + requestIDs = c.getPendingRequestIds(); + + assertEq(requestIDs.length, 2); + assertEq(requestIDs[0], req2, "First should be req2"); + assertEq(requestIDs[1], req4, "Second should be req4"); + vm.stopPrank(); + + vm.prank(c.owner()); + c.dropRequests(requestIDs); + + assertEq(c.getPendingRequestIds().length, 0); + } }