From 06b4cb93131d06475954d320d46a5692d89c3334 Mon Sep 17 00:00:00 2001 From: liobrasil Date: Mon, 19 Jan 2026 18:38:24 -0400 Subject: [PATCH 1/3] fix(FLOW-7): enforce sentinel hygiene for NO_YIELDVAULT_ID Prevents corruption of "no yieldvault" semantics: 1. Added guard in _registerYieldVault() to reject NO_YIELDVAULT_ID 2. Changed createYieldVault() to use NO_YIELDVAULT_ID as placeholder instead of 0 (which can be a valid Cadence yieldVaultId) 3. Added CannotRegisterSentinelYieldVaultId custom error 4. Updated documentation to reflect new behavior Co-Authored-By: Claude Opus 4.5 --- solidity/src/FlowYieldVaultsRequests.sol | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/solidity/src/FlowYieldVaultsRequests.sol b/solidity/src/FlowYieldVaultsRequests.sol index 14ef1db..0a5e1da 100644 --- a/solidity/src/FlowYieldVaultsRequests.sol +++ b/solidity/src/FlowYieldVaultsRequests.sol @@ -61,7 +61,7 @@ contract FlowYieldVaultsRequests is ReentrancyGuard, Ownable2Step { /// @param status Current status of the request /// @param tokenAddress Token being deposited/withdrawn (NATIVE_FLOW for native $FLOW) /// @param amount Amount of tokens involved - /// @param yieldVaultId Associated YieldVault Id (0 for CREATE_YIELDVAULT until completed; NO_YIELDVAULT_ID only on failed CREATE) + /// @param yieldVaultId Associated YieldVault Id (NO_YIELDVAULT_ID for CREATE_YIELDVAULT until assigned by Cadence) /// @param timestamp Block timestamp when request was created /// @param message Status message or error reason /// @param vaultIdentifier Cadence vault type identifier for CREATE_YIELDVAULT @@ -245,6 +245,9 @@ contract FlowYieldVaultsRequests is ReentrancyGuard, Ownable2Step { /// @notice YieldVault token is not set error YieldVaultTokenNotSet(uint64 yieldVaultId); + /// @notice Cannot register sentinel value NO_YIELDVAULT_ID as a valid YieldVault + error CannotRegisterSentinelYieldVaultId(); + /// @notice Token does not match YieldVault's configured token error YieldVaultTokenMismatch( uint64 yieldVaultId, @@ -274,7 +277,7 @@ contract FlowYieldVaultsRequests is ReentrancyGuard, Ownable2Step { /// @param requestType Type of operation requested /// @param tokenAddress Token involved in the request /// @param amount Amount of tokens - /// @param yieldVaultId Associated YieldVault Id (0 for CREATE_YIELDVAULT until assigned by Cadence; NO_YIELDVAULT_ID only on failed CREATE) + /// @param yieldVaultId Associated YieldVault Id (NO_YIELDVAULT_ID for CREATE_YIELDVAULT until assigned by Cadence) /// @param timestamp Block timestamp when request was created /// @param vaultIdentifier Cadence vault type identifier (for CREATE_YIELDVAULT) /// @param strategyIdentifier Cadence strategy type identifier (for CREATE_YIELDVAULT) @@ -792,7 +795,7 @@ contract FlowYieldVaultsRequests is ReentrancyGuard, Ownable2Step { RequestType.CREATE_YIELDVAULT, tokenAddress, amount, - 0, + NO_YIELDVAULT_ID, vaultIdentifier, strategyIdentifier ); @@ -1545,6 +1548,9 @@ contract FlowYieldVaultsRequests is ReentrancyGuard, Ownable2Step { address tokenAddress, uint256 requestId ) internal { + // Reject sentinel value to prevent corruption of "no yieldvault" semantics + if (yieldVaultId == NO_YIELDVAULT_ID) revert CannotRegisterSentinelYieldVaultId(); + // Mark YieldVault as valid and set owner validYieldVaultIds[yieldVaultId] = true; yieldVaultOwners[yieldVaultId] = user; @@ -1612,7 +1618,7 @@ contract FlowYieldVaultsRequests is ReentrancyGuard, Ownable2Step { * @param requestType The type of request (CREATE, DEPOSIT, WITHDRAW, CLOSE). * @param tokenAddress The token involved in this request. * @param amount The amount of tokens involved (0 for CLOSE requests). - * @param yieldVaultId The YieldVault Id (0 for CREATE until assigned by Cadence; NO_YIELDVAULT_ID only on failed CREATE). + * @param yieldVaultId The YieldVault Id (NO_YIELDVAULT_ID for CREATE until assigned by Cadence). * @param vaultIdentifier Cadence vault type identifier (only for CREATE requests). * @param strategyIdentifier Cadence strategy type identifier (only for CREATE requests). * @return The newly created request ID. From 69676c781ab78160f0c512681f323026a4cb8fbe Mon Sep 17 00:00:00 2001 From: liobrasil Date: Mon, 19 Jan 2026 18:51:50 -0400 Subject: [PATCH 2/3] docs,test(FLOW-7): update docs and add sentinel placeholder test --- FLOW_YIELD_VAULTS_EVM_BRIDGE_DESIGN.md | 2 +- FRONTEND_INTEGRATION.md | 4 ++-- solidity/test/FlowYieldVaultsRequests.t.sol | 26 +++++++++++++++++++++ 3 files changed, 29 insertions(+), 3 deletions(-) diff --git a/FLOW_YIELD_VAULTS_EVM_BRIDGE_DESIGN.md b/FLOW_YIELD_VAULTS_EVM_BRIDGE_DESIGN.md index 4c57ab7..5242c33 100644 --- a/FLOW_YIELD_VAULTS_EVM_BRIDGE_DESIGN.md +++ b/FLOW_YIELD_VAULTS_EVM_BRIDGE_DESIGN.md @@ -195,7 +195,7 @@ struct Request { RequestStatus status; // PENDING | PROCESSING | COMPLETED | FAILED address tokenAddress; // NATIVE_FLOW (0xFFfF...FfFFFfF) or ERC20 address uint256 amount; // Amount in wei (0 for CLOSE_YIELDVAULT) - uint64 yieldVaultId; // Target YieldVault Id (0 for CREATE_YIELDVAULT until completed; NO_YIELDVAULT_ID on failed CREATE during processing; cancel/drop keep 0) + uint64 yieldVaultId; // Target YieldVault Id (NO_YIELDVAULT_ID for CREATE_YIELDVAULT until completed; for others set at request creation) uint256 timestamp; // Block timestamp when created string message; // Status message or error reason string vaultIdentifier; // Cadence vault type (e.g., "A.xxx.FlowToken.Vault") diff --git a/FRONTEND_INTEGRATION.md b/FRONTEND_INTEGRATION.md index 860bcc5..140d465 100644 --- a/FRONTEND_INTEGRATION.md +++ b/FRONTEND_INTEGRATION.md @@ -607,9 +607,9 @@ if (wallet.type === "evm") { // Sentinel address for native FLOW token const NATIVE_FLOW = "0xFFfFfFffFFfffFFfFFfFFFFFffFFFffffFfFFFfF"; -// Sentinel value for "no YieldVault" (Cadence may return this on failed CREATE) +// Sentinel value for "no YieldVault" (also used as placeholder for CREATE until processed) const NO_YIELDVAULT_ID = 18446744073709551615n; // type(uint64).max -// CREATE requests start with yieldVaultId = 0 until processed +// CREATE requests start with yieldVaultId = NO_YIELDVAULT_ID until processed // Request Types enum RequestType { diff --git a/solidity/test/FlowYieldVaultsRequests.t.sol b/solidity/test/FlowYieldVaultsRequests.t.sol index 9336d75..68098f0 100644 --- a/solidity/test/FlowYieldVaultsRequests.t.sol +++ b/solidity/test/FlowYieldVaultsRequests.t.sol @@ -66,6 +66,32 @@ contract FlowYieldVaultsRequestsTest is Test { assertEq(req.amount, 1 ether); } + function test_CreateYieldVault_UsesSentinelYieldVaultIdPlaceholder() public { + vm.prank(user); + uint256 reqId = c.createYieldVault{value: 1 ether}(NATIVE_FLOW, 1 ether, VAULT_ID, STRATEGY_ID); + + uint64 sentinelYieldVaultId = c.NO_YIELDVAULT_ID(); + FlowYieldVaultsRequests.Request memory req = c.getRequest(reqId); + assertEq( + req.yieldVaultId, + sentinelYieldVaultId, + "CREATE should start with NO_YIELDVAULT_ID" + ); + + vm.startPrank(coa); + c.startProcessing(reqId); + vm.expectRevert( + FlowYieldVaultsRequests.CannotRegisterSentinelYieldVaultId.selector + ); + c.completeProcessing( + reqId, + true, + sentinelYieldVaultId, + "Invalid yieldVaultId" + ); + vm.stopPrank(); + } + function test_DepositToYieldVault() public { vm.prank(user); uint256 reqId = c.depositToYieldVault{value: 1 ether}(42, NATIVE_FLOW, 1 ether); From 3d6b0309695410dbc1a50219adf8744fa7a69941 Mon Sep 17 00:00:00 2001 From: liobrasil Date: Wed, 28 Jan 2026 21:01:18 -0400 Subject: [PATCH 3/3] test: use NO_YIELDVAULT_ID consistently --- cadence/tests/error_handling_test.cdc | 2 +- cadence/tests/evm_bridge_lifecycle_test.cdc | 12 +++++----- solidity/test/FlowYieldVaultsRequests.t.sol | 26 +++++++++++++++++---- 3 files changed, 28 insertions(+), 12 deletions(-) diff --git a/cadence/tests/error_handling_test.cdc b/cadence/tests/error_handling_test.cdc index 14d9ece..1016c48 100644 --- a/cadence/tests/error_handling_test.cdc +++ b/cadence/tests/error_handling_test.cdc @@ -58,7 +58,7 @@ fun testInvalidRequestType() { status: FlowYieldVaultsEVM.RequestStatus.PENDING.rawValue, tokenAddress: nativeFlowAddr, amount: 1000000000000000000, - yieldVaultId: 0, + yieldVaultId: FlowYieldVaultsEVM.noYieldVaultId, timestamp: 0, message: "", vaultIdentifier: mockVaultIdentifier, diff --git a/cadence/tests/evm_bridge_lifecycle_test.cdc b/cadence/tests/evm_bridge_lifecycle_test.cdc index 3434a4a..eba5e31 100644 --- a/cadence/tests/evm_bridge_lifecycle_test.cdc +++ b/cadence/tests/evm_bridge_lifecycle_test.cdc @@ -50,7 +50,7 @@ fun testCreateYieldVaultFromEVMRequest() { status: FlowYieldVaultsEVM.RequestStatus.PENDING.rawValue, tokenAddress: nativeFlowAddr, amount: 1000000000000000000, // 1 FLOW in wei (10^18) - yieldVaultId: 0, // Not used for CREATE + yieldVaultId: FlowYieldVaultsEVM.noYieldVaultId, // Placeholder until Cadence assigns a real ID timestamp: 0, message: "", vaultIdentifier: mockVaultIdentifier, @@ -161,7 +161,7 @@ fun testRequestStatusTransitions() { status: FlowYieldVaultsEVM.RequestStatus.COMPLETED.rawValue, tokenAddress: nativeFlowAddr, amount: 1000000000000000000, - yieldVaultId: 0, + yieldVaultId: FlowYieldVaultsEVM.noYieldVaultId, timestamp: 0, message: "", vaultIdentifier: mockVaultIdentifier, @@ -177,7 +177,7 @@ fun testRequestStatusTransitions() { status: FlowYieldVaultsEVM.RequestStatus.FAILED.rawValue, tokenAddress: nativeFlowAddr, amount: 1000000000000000000, - yieldVaultId: 0, + yieldVaultId: FlowYieldVaultsEVM.noYieldVaultId, timestamp: 0, message: "Insufficient balance", vaultIdentifier: mockVaultIdentifier, @@ -197,7 +197,7 @@ fun testMultipleUsersIndependentYieldVaults() { status: FlowYieldVaultsEVM.RequestStatus.PENDING.rawValue, tokenAddress: nativeFlowAddr, amount: 1000000000000000000, - yieldVaultId: 0, + yieldVaultId: FlowYieldVaultsEVM.noYieldVaultId, timestamp: 0, message: "", vaultIdentifier: mockVaultIdentifier, @@ -211,7 +211,7 @@ fun testMultipleUsersIndependentYieldVaults() { status: FlowYieldVaultsEVM.RequestStatus.PENDING.rawValue, tokenAddress: nativeFlowAddr, amount: 2000000000000000000, - yieldVaultId: 0, + yieldVaultId: FlowYieldVaultsEVM.noYieldVaultId, timestamp: 0, message: "", vaultIdentifier: mockVaultIdentifier, @@ -268,7 +268,7 @@ fun testVaultAndStrategyIdentifiers() { status: FlowYieldVaultsEVM.RequestStatus.PENDING.rawValue, tokenAddress: nativeFlowAddr, amount: 1000000000000000000, - yieldVaultId: 0, + yieldVaultId: FlowYieldVaultsEVM.noYieldVaultId, timestamp: 0, message: "", vaultIdentifier: customVaultId, diff --git a/solidity/test/FlowYieldVaultsRequests.t.sol b/solidity/test/FlowYieldVaultsRequests.t.sol index 68098f0..51ba0c7 100644 --- a/solidity/test/FlowYieldVaultsRequests.t.sol +++ b/solidity/test/FlowYieldVaultsRequests.t.sol @@ -92,6 +92,19 @@ contract FlowYieldVaultsRequestsTest is Test { vm.stopPrank(); } + function test_CreateYieldVault_CanRegisterZeroYieldVaultId() 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, 0, "YieldVault 0 created"); + vm.stopPrank(); + + assertTrue(c.isYieldVaultIdValid(0), "YieldVault ID 0 should be valid"); + assertTrue(c.doesUserOwnYieldVault(user, 0), "User should own YieldVault 0"); + } + function test_DepositToYieldVault() public { vm.prank(user); uint256 reqId = c.depositToYieldVault{value: 1 ether}(42, NATIVE_FLOW, 1 ether); @@ -215,8 +228,9 @@ contract FlowYieldVaultsRequestsTest is Test { assertEq(c.getUserPendingBalance(user, NATIVE_FLOW), 0); // 3. COA fails and returns funds + uint64 sentinelYieldVaultId = c.NO_YIELDVAULT_ID(); vm.prank(coa); - c.completeProcessing{value: 1 ether}(reqId, false, 0, "Failed"); + c.completeProcessing{value: 1 ether}(reqId, false, sentinelYieldVaultId, "Failed"); // 4. User has refund in claimableRefunds (not pendingUserBalances) assertEq(c.getUserPendingBalance(user, NATIVE_FLOW), 0); @@ -248,8 +262,9 @@ contract FlowYieldVaultsRequestsTest is Test { // Process and fail vm.prank(coa); c.startProcessing(reqId); + uint64 sentinelYieldVaultId = c.NO_YIELDVAULT_ID(); vm.prank(coa); - c.completeProcessing{value: 2 ether}(reqId, false, 0, "Failed"); + c.completeProcessing{value: 2 ether}(reqId, false, sentinelYieldVaultId, "Failed"); // Claim only NATIVE_FLOW uint256 balBefore = user.balance; @@ -266,8 +281,9 @@ contract FlowYieldVaultsRequestsTest is Test { vm.prank(coa); c.startProcessing(reqId); + uint64 sentinelYieldVaultId = c.NO_YIELDVAULT_ID(); vm.prank(coa); - c.completeProcessing{value: 1 ether}(reqId, false, 0, "Failed"); + c.completeProcessing{value: 1 ether}(reqId, false, sentinelYieldVaultId, "Failed"); // RefundClaimed is emitted on claim (no BalanceUpdated since we use separate claimableRefunds mapping) vm.prank(user); @@ -347,7 +363,7 @@ contract FlowYieldVaultsRequestsTest is Test { assertEq(c.getClaimableRefund(user, NATIVE_FLOW), 0); // COA must return funds when completing with failure - c.completeProcessing{value: 1 ether}(reqId, false, 0, "Cadence error"); + c.completeProcessing{value: 1 ether}(reqId, false, c.NO_YIELDVAULT_ID(), "Cadence error"); vm.stopPrank(); // Funds go to claimableRefunds (not pendingUserBalances) @@ -1286,7 +1302,7 @@ contract FlowYieldVaultsRequestsTest is Test { vm.startPrank(coa); c.startProcessing(req1); // COA must return funds when completing with failure - c.completeProcessing{value: 1 ether}(req1, false, 0, "Failed"); + c.completeProcessing{value: 1 ether}(req1, false, c.NO_YIELDVAULT_ID(), "Failed"); vm.stopPrank(); // req1 should still be removed from pending (it's marked FAILED)