From 5b6b65b578e17505843e81b632a86239f87eded2 Mon Sep 17 00:00:00 2001 From: Ardit Marku Date: Fri, 23 Jan 2026 15:54:00 +0200 Subject: [PATCH 1/4] Add checks to guard against local registry YieldVault IDs corruption --- solidity/src/FlowYieldVaultsRequests.sol | 25 ++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/solidity/src/FlowYieldVaultsRequests.sol b/solidity/src/FlowYieldVaultsRequests.sol index 14ef1db..351d22d 100644 --- a/solidity/src/FlowYieldVaultsRequests.sol +++ b/solidity/src/FlowYieldVaultsRequests.sol @@ -242,6 +242,12 @@ contract FlowYieldVaultsRequests is ReentrancyGuard, Ownable2Step { /// @notice YieldVault Id is invalid or not owned by user error InvalidYieldVaultId(uint64 yieldVaultId, address user); + /// @notice YieldVault Id has already been registered + error YieldVaultIdAlreadyRegistered(uint64 yieldVaultId); + + /// @notice YieldVault Id does not match the request's value + error YieldVaultIdMismatch(uint64 expectedId, uint64 providedId); + /// @notice YieldVault token is not set error YieldVaultTokenNotSet(uint64 yieldVaultId); @@ -1110,6 +1116,15 @@ contract FlowYieldVaultsRequests is ReentrancyGuard, Ownable2Step { : RequestStatus.FAILED; request.status = newStatus; request.message = message; + + // Enforce strong ID binding for DEPOSIT/WITHDRAW/CLOSE by requiring the + // supplied yieldVaultId matches the request's stored yieldVaultId + if (request.requestType == RequestType.CREATE_YIELDVAULT) { + request.yieldVaultId = yieldVaultId; + } else if (request.yieldVaultId != yieldVaultId) { + revert YieldVaultIdMismatch(request.yieldVaultId, yieldVaultId); + } + request.yieldVaultId = yieldVaultId; // === HANDLE REFUNDS FOR FAILED CREATE/DEPOSIT === @@ -1545,6 +1560,11 @@ contract FlowYieldVaultsRequests is ReentrancyGuard, Ownable2Step { address tokenAddress, uint256 requestId ) internal { + // Uniqueness guard, reject registering an already-valid yieldVaultId + if (validYieldVaultIds[yieldVaultId]) { + revert YieldVaultIdAlreadyRegistered(yieldVaultId); + } + // Mark YieldVault as valid and set owner validYieldVaultIds[yieldVaultId] = true; yieldVaultOwners[yieldVaultId] = user; @@ -1570,6 +1590,11 @@ contract FlowYieldVaultsRequests is ReentrancyGuard, Ownable2Step { * @param requestId The CLOSE_YIELDVAULT request ID that closed this YieldVault. */ function _unregisterYieldVault(uint64 yieldVaultId, address user, uint256 requestId) internal { + // Prove the yieldVaultId is actually registered under the provided user + if (yieldVaultOwners[yieldVaultId] != user) { + revert InvalidYieldVaultId(yieldVaultId, user); + } + uint64[] storage userYieldVaults = yieldVaultsByUser[user]; uint256 indexToRemove = _yieldVaultIndexInUserArray[user][yieldVaultId]; From f3d3f601c51944f708d6fa2ddbdc7758a0ff13e5 Mon Sep 17 00:00:00 2001 From: Ardit Marku Date: Wed, 28 Jan 2026 13:52:25 +0200 Subject: [PATCH 2/4] Remove redundant assignment of yieldVaultId on request --- solidity/src/FlowYieldVaultsRequests.sol | 2 -- 1 file changed, 2 deletions(-) diff --git a/solidity/src/FlowYieldVaultsRequests.sol b/solidity/src/FlowYieldVaultsRequests.sol index 351d22d..78f0714 100644 --- a/solidity/src/FlowYieldVaultsRequests.sol +++ b/solidity/src/FlowYieldVaultsRequests.sol @@ -1125,8 +1125,6 @@ contract FlowYieldVaultsRequests is ReentrancyGuard, Ownable2Step { revert YieldVaultIdMismatch(request.yieldVaultId, yieldVaultId); } - request.yieldVaultId = yieldVaultId; - // === HANDLE REFUNDS FOR FAILED CREATE/DEPOSIT === // COA must return the funds that were transferred in startProcessing if ( From fe984b42d2fbd7efec640bfe8c7bae0f764efca9 Mon Sep 17 00:00:00 2001 From: Ardit Marku Date: Wed, 28 Jan 2026 17:04:26 +0200 Subject: [PATCH 3/4] Add unit tests for validation logic --- solidity/test/FlowYieldVaultsRequests.t.sol | 47 +++++++++++++++++++++ 1 file changed, 47 insertions(+) diff --git a/solidity/test/FlowYieldVaultsRequests.t.sol b/solidity/test/FlowYieldVaultsRequests.t.sol index 9336d75..5c64fa2 100644 --- a/solidity/test/FlowYieldVaultsRequests.t.sol +++ b/solidity/test/FlowYieldVaultsRequests.t.sol @@ -1293,4 +1293,51 @@ contract FlowYieldVaultsRequestsTest is Test { (uint256[] memory userIds, , , , , , , , , , , ) = c.getPendingRequestsByUserUnpacked(user); assertEq(userIds.length, 2); } + + // Test that duplicate registration is blocked + function test_RegisterYieldVault_RevertAlreadyRegistered() public { + vm.startPrank(user); + uint256 reqId = c.createYieldVault{value: 1 ether}(NATIVE_FLOW, 1 ether, VAULT_ID, STRATEGY_ID); + vm.stopPrank(); + + vm.startPrank(coa); + c.startProcessing(reqId); + c.completeProcessing(reqId, true, 100, "Created"); + + // Try to register same ID again (simulate COA bug) + uint256 reqId2 = c.createYieldVault{value: 1 ether}(NATIVE_FLOW, 1 ether, VAULT_ID, STRATEGY_ID); + c.startProcessing(reqId2); + vm.expectRevert(abi.encodeWithSelector( + FlowYieldVaultsRequests.YieldVaultIdAlreadyRegistered.selector, + 100 + )); + c.completeProcessing(reqId2, true, 100, "Duplicate"); + vm.stopPrank(); + } + + // Test that ID mismatch on DEPOSIT is blocked + function test_CompleteProcessing_RevertDepositIdMismatch() public { + vm.startPrank(user); + uint256 reqId = c.createYieldVault{value: 1 ether}(NATIVE_FLOW, 1 ether, VAULT_ID, STRATEGY_ID); + vm.stopPrank(); + + vm.startPrank(coa); + c.startProcessing(reqId); + c.completeProcessing(reqId, true, 100, "Created"); + vm.stopPrank(); + + // Try to deposit but COA provides wrong ID + vm.prank(user); + uint256 depositReq = c.depositToYieldVault{value: 1 ether}(100, NATIVE_FLOW, 1 ether); + + vm.startPrank(coa); + c.startProcessing(depositReq); + vm.expectRevert(abi.encodeWithSelector( + FlowYieldVaultsRequests.YieldVaultIdMismatch.selector, + 100, // expected + 101 // provided (wrong) + )); + c.completeProcessing(depositReq, true, 101, "Wrong ID"); + vm.stopPrank(); + } } From 7e6c739587033b73328bedda04becc6abffdd068 Mon Sep 17 00:00:00 2001 From: Ardit Marku Date: Wed, 28 Jan 2026 17:45:52 +0200 Subject: [PATCH 4/4] Add more unit tests for the new validation logic --- solidity/test/FlowYieldVaultsRequests.t.sol | 25 +++++++++++++++++---- 1 file changed, 21 insertions(+), 4 deletions(-) diff --git a/solidity/test/FlowYieldVaultsRequests.t.sol b/solidity/test/FlowYieldVaultsRequests.t.sol index 5c64fa2..3cc18c3 100644 --- a/solidity/test/FlowYieldVaultsRequests.t.sol +++ b/solidity/test/FlowYieldVaultsRequests.t.sol @@ -1296,9 +1296,8 @@ contract FlowYieldVaultsRequestsTest is Test { // Test that duplicate registration is blocked function test_RegisterYieldVault_RevertAlreadyRegistered() public { - vm.startPrank(user); + vm.prank(user); uint256 reqId = c.createYieldVault{value: 1 ether}(NATIVE_FLOW, 1 ether, VAULT_ID, STRATEGY_ID); - vm.stopPrank(); vm.startPrank(coa); c.startProcessing(reqId); @@ -1317,9 +1316,8 @@ contract FlowYieldVaultsRequestsTest is Test { // Test that ID mismatch on DEPOSIT is blocked function test_CompleteProcessing_RevertDepositIdMismatch() public { - vm.startPrank(user); + vm.prank(user); uint256 reqId = c.createYieldVault{value: 1 ether}(NATIVE_FLOW, 1 ether, VAULT_ID, STRATEGY_ID); - vm.stopPrank(); vm.startPrank(coa); c.startProcessing(reqId); @@ -1340,4 +1338,23 @@ contract FlowYieldVaultsRequestsTest is Test { c.completeProcessing(depositReq, true, 101, "Wrong ID"); vm.stopPrank(); } + + // Test for unregister with wrong user ownership + function test_CompleteProcessing_RevertInvalidYieldVaultId() public { + vm.prank(user); + uint256 reqId = c.createYieldVault{value: 1 ether}(NATIVE_FLOW, 1 ether, VAULT_ID, STRATEGY_ID); + + vm.startPrank(coa); + c.startProcessing(reqId); + c.completeProcessing(reqId, true, 100, "Created"); + vm.stopPrank(); + + vm.prank(user2); + vm.expectRevert(abi.encodeWithSelector( + FlowYieldVaultsRequests.InvalidYieldVaultId.selector, + 100, + user2 + )); + uint256 closeReq = c.closeYieldVault(100); + } }