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/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/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. diff --git a/solidity/test/FlowYieldVaultsRequests.t.sol b/solidity/test/FlowYieldVaultsRequests.t.sol index 9336d75..51ba0c7 100644 --- a/solidity/test/FlowYieldVaultsRequests.t.sol +++ b/solidity/test/FlowYieldVaultsRequests.t.sol @@ -66,6 +66,45 @@ 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_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); @@ -189,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); @@ -222,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; @@ -240,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); @@ -321,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) @@ -1260,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)