Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion FLOW_YIELD_VAULTS_EVM_BRIDGE_DESIGN.md
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down
4 changes: 2 additions & 2 deletions FRONTEND_INTEGRATION.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
2 changes: 1 addition & 1 deletion cadence/tests/error_handling_test.cdc
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
12 changes: 6 additions & 6 deletions cadence/tests/evm_bridge_lifecycle_test.cdc
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand All @@ -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,
Expand All @@ -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,
Expand All @@ -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,
Expand Down Expand Up @@ -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,
Expand Down
14 changes: 10 additions & 4 deletions solidity/src/FlowYieldVaultsRequests.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -792,7 +795,7 @@ contract FlowYieldVaultsRequests is ReentrancyGuard, Ownable2Step {
RequestType.CREATE_YIELDVAULT,
tokenAddress,
amount,
0,
NO_YIELDVAULT_ID,
vaultIdentifier,
strategyIdentifier
);
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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.
Expand Down
52 changes: 47 additions & 5 deletions solidity/test/FlowYieldVaultsRequests.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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;
Expand All @@ -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);
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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)
Expand Down