diff --git a/.github/workflows/e2e_test.yml b/.github/workflows/e2e_test.yml index 77e0bd6..7cd450a 100644 --- a/.github/workflows/e2e_test.yml +++ b/.github/workflows/e2e_test.yml @@ -80,32 +80,6 @@ jobs: echo "CONTRACT_ADDRESS=$FLOW_VAULTS_REQUESTS_CONTRACT" >> $GITHUB_ENV - - name: Detect Strategy Identifier - run: | - echo "Detecting supported strategy identifier..." - YIELDVAULT_CHECK=$(flow scripts execute ./cadence/scripts/check_yieldvault_details.cdc 0x045a1763c93006ca) - echo "$YIELDVAULT_CHECK" - - SUPPORTED_STRATEGIES=$(echo "$YIELDVAULT_CHECK" | grep -oE '"supportedStrategies": \[[^]]*\]' || true) - if [ -z "$SUPPORTED_STRATEGIES" ]; then - echo "❌ Could not parse supported strategy list" - exit 1 - fi - - STRATEGY_LIST=$(echo "$SUPPORTED_STRATEGIES" | sed -E 's/^"supportedStrategies": \[(.*)\]$/\1/' | tr -d '"' | tr ',' '\n' | sed 's/^ *//;s/ *$//' | sed '/^$/d') - STRATEGY_IDENTIFIER=$(echo "$STRATEGY_LIST" | grep 'TracerStrategy' | head -n 1 || true) - if [ -z "$STRATEGY_IDENTIFIER" ]; then - STRATEGY_IDENTIFIER=$(echo "$STRATEGY_LIST" | head -n 1) - fi - - if [ -z "$STRATEGY_IDENTIFIER" ]; then - echo "❌ No supported strategy identifier found" - exit 1 - fi - - echo "Using strategy identifier: $STRATEGY_IDENTIFIER" - echo "STRATEGY_IDENTIFIER=$STRATEGY_IDENTIFIER" >> $GITHUB_ENV - # === TEST 1: BASIC YIELDVAULT CREATION === - name: Test 1 - Create YieldVault (10 FLOW) run: | @@ -120,6 +94,7 @@ jobs: --legacy env: AMOUNT: 10000000000000000000 + CREATE_VAULT_CONFIG_ID: 1 - name: Process Create Request run: | diff --git a/.github/workflows/worker_tests.yml b/.github/workflows/worker_tests.yml index 00cb304..fdbd39c 100644 --- a/.github/workflows/worker_tests.yml +++ b/.github/workflows/worker_tests.yml @@ -55,32 +55,6 @@ jobs: - name: Deploy Full Stack run: ./local/deploy_full_stack.sh - - name: Detect Strategy Identifier - run: | - echo "Detecting supported strategy identifier..." - YIELDVAULT_CHECK=$(flow scripts execute ./cadence/scripts/check_yieldvault_details.cdc 0x045a1763c93006ca) - echo "$YIELDVAULT_CHECK" - - SUPPORTED_STRATEGIES=$(echo "$YIELDVAULT_CHECK" | grep -oE '"supportedStrategies": \[[^]]*\]' || true) - if [ -z "$SUPPORTED_STRATEGIES" ]; then - echo "❌ Could not parse supported strategy list" - exit 1 - fi - - STRATEGY_LIST=$(echo "$SUPPORTED_STRATEGIES" | sed -E 's/^"supportedStrategies": \[(.*)\]$/\1/' | tr -d '"' | tr ',' '\n' | sed 's/^ *//;s/ *$//' | sed '/^$/d') - STRATEGY_IDENTIFIER=$(echo "$STRATEGY_LIST" | grep 'TracerStrategy' | head -n 1 || true) - if [ -z "$STRATEGY_IDENTIFIER" ]; then - STRATEGY_IDENTIFIER=$(echo "$STRATEGY_LIST" | head -n 1) - fi - - if [ -z "$STRATEGY_IDENTIFIER" ]; then - echo "❌ No supported strategy identifier found" - exit 1 - fi - - echo "Using strategy identifier: $STRATEGY_IDENTIFIER" - echo "STRATEGY_IDENTIFIER=$STRATEGY_IDENTIFIER" >> $GITHUB_ENV - # === RUN WORKER TESTS === - name: Run Worker Tests run: ./local/run_worker_tests.sh diff --git a/CLAUDE.md b/CLAUDE.md index da59ae4..742ea1f 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -47,7 +47,7 @@ flow deps install --skip-alias --skip-deployments # Install dependencies 1. **EVM User** calls `FlowYieldVaultsRequests.sol` (creates request, escrows funds) 2. **FlowYieldVaultsEVMWorkerOps.cdc** SchedulerHandler schedules WorkerHandlers to process requests -3. **FlowYieldVaultsEVM.cdc** Worker fetches pending requests via `getPendingRequestsUnpacked()` +3. **FlowYieldVaultsEVM.cdc** Worker fetches pending requests via `getPendingRequestsUnpacked()`, then resolves `createVaultConfigId` against the local Cadence config registry for `CREATE_YIELDVAULT` 4. **Two-phase commit**: `startProcessingBatch()` marks PROCESSING and deducts balance, `completeProcessing()` marks COMPLETED/FAILED (refunds credited to `claimableRefunds` on failure) ### Contract Components @@ -61,6 +61,7 @@ flow deps install --skip-alias --skip-deployments # Install dependencies ### Key Design Patterns - **COA Bridge**: Cadence Owned Account bridges funds between EVM and Cadence via FlowEVMBridge +- **Immutable CREATE Configs**: EVM requests store `createVaultConfigId`; Cadence resolves identifiers locally; config onboarding should use `cadence/transactions/register_create_yieldvault_config_everywhere.cdc` - **Sentinel Values**: `NATIVE_FLOW = 0xFFfFfFffFFfffFFfFFfFFFFFffFFFffffFfFFFfF`, `NO_YIELDVAULT_ID = type(uint64).max` - **Ownership Tracking**: Parallel mappings on both EVM (`userOwnsYieldVault`) and Cadence (`yieldVaultOwnershipLookup`) for O(1) lookups - **Scheduler/Worker Split**: SchedulerHandler runs at fixed interval, schedules WorkerHandlers for individual requests diff --git a/FLOW_YIELD_VAULTS_EVM_BRIDGE_DESIGN.md b/FLOW_YIELD_VAULTS_EVM_BRIDGE_DESIGN.md index 74bf117..5badefe 100644 --- a/FLOW_YIELD_VAULTS_EVM_BRIDGE_DESIGN.md +++ b/FLOW_YIELD_VAULTS_EVM_BRIDGE_DESIGN.md @@ -84,6 +84,7 @@ Request queue and fund escrow contract. **Responsibilities:** - Accept and queue user requests (CREATE_YIELDVAULT, DEPOSIT_TO_YIELDVAULT, WITHDRAW_FROM_YIELDVAULT, CLOSE_YIELDVAULT) +- Maintain the immutable CREATE_YIELDVAULT config registry keyed by `createVaultConfigId` - Escrow deposited funds until processing - Track user balances and pending request counts - Enforce access control (allowlist/blocklist) @@ -126,6 +127,7 @@ Worker contract that processes EVM requests and manages YieldVault positions. **Responsibilities:** - Fetch pending requests from EVM via `getPendingRequestsUnpacked()` +- Resolve `createVaultConfigId` values against the local Cadence config registry before processing `CREATE_YIELDVAULT` - Execute two-phase commit (startProcessingBatch → operation → completeProcessing) - Create, deposit to, withdraw from, and close YieldVaults - Bridge funds between EVM and Cadence via COA @@ -136,7 +138,10 @@ Worker contract that processes EVM requests and manages YieldVault positions. // YieldVault ownership tracking access(all) let yieldVaultRegistry: {String: {UInt64: Bool}} -// Configuration (stored as contract-only vars; exposed via getters) +// Registered immutable CREATE_YIELDVAULT configs +access(all) let createYieldVaultConfigs: {UInt64: CreateYieldVaultConfig} + +// Configuration var flowYieldVaultsRequestsAddress: EVM.EVMAddress? // Constants @@ -211,10 +216,31 @@ struct Request { 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 + uint64 createVaultConfigId; // Immutable CREATE_YIELDVAULT config ID (0 for non-create requests) +} + +struct RequestView { + uint256 id; + address user; + RequestType requestType; + RequestStatus status; + address tokenAddress; + uint256 amount; + uint64 yieldVaultId; + uint256 timestamp; + string message; + uint64 createVaultConfigId; string vaultIdentifier; // Cadence vault type (e.g., "A.xxx.FlowToken.Vault") string strategyIdentifier; // Cadence strategy type (e.g., "A.xxx.Strategy.Type") } +struct CreateYieldVaultConfig { + bool exists; + bool enabled; + string vaultIdentifier; + string strategyIdentifier; +} + enum RequestType { CREATE_YIELDVAULT, // 0 DEPOSIT_TO_YIELDVAULT, // 1 @@ -236,6 +262,8 @@ struct TokenConfig { } ``` +`Request` is the canonical stored payload on EVM. `RequestView` is the resolved read model returned by `getRequest()`. + ### EVMRequest (Cadence) ```cadence @@ -246,14 +274,17 @@ access(all) struct EVMRequest { access(all) let status: UInt8 access(all) let tokenAddress: EVM.EVMAddress access(all) let amount: UInt256 - access(all) let yieldVaultId: UInt64 + access(all) let yieldVaultId: UInt64? access(all) let timestamp: UInt256 access(all) let message: String + access(all) let createVaultConfigId: UInt64? access(all) let vaultIdentifier: String access(all) let strategyIdentifier: String } ``` +`getPendingRequestsUnpacked()` returns `createVaultConfigId` values only. The worker resolves `vaultIdentifier` and `strategyIdentifier` from the Cadence config registry before constructing `EVMRequest`. + ### ProcessResult (Cadence) ```cadence @@ -280,7 +311,7 @@ access(all) struct ProcessResult { │ │ │ │ │ createYieldVault( │ │ │ │ token, amount, │ │ │ - │ vault, strategy) │ │ │ + │ createVaultConfigId)│ │ │ │────────────────────▶│ │ │ │ │ Escrow funds │ │ │ │ Create PENDING request │ │ @@ -289,7 +320,8 @@ access(all) struct ProcessResult { │ │ │ │ │ │ getPendingRequestsUnpacked │ │ │ │◀───────────────────────│ │ - │ │ [EVMRequest] │ │ + │ │ [request arrays + │ │ + │ │ createVaultConfigIds] │ │ │ │───────────────────────▶│ │ │ │ │ │ │ │ startProcessingBatch([id], []) │ │ @@ -321,6 +353,18 @@ access(all) struct ProcessResult { │ │───────────────────────▶│ │ ``` +Canonical CREATE requests carry `createVaultConfigId` on EVM. The legacy `(vaultIdentifier, strategyIdentifier)` overload resolves the pair to a registered config ID before the request is stored. + +### CREATE_YIELDVAULT Config Registration + +``` +1. Admin submits register_create_yieldvault_config_everywhere.cdc +2. Transaction borrows FlowYieldVaultsEVM.Admin and FlowYieldVaultsEVM.Worker from the same signer account +3. Admin validates the identifiers locally and writes the Cadence config registry entry +4. Worker calls Solidity registerCreateYieldVaultConfig(uint64,string,string) through the COA +5. If the EVM call fails, the entire Cadence transaction reverts and the Cadence write is rolled back +``` + ### DEPOSIT_TO_YIELDVAULT ``` @@ -544,14 +588,43 @@ function doesUserOwnYieldVault(address user, uint64 yieldVaultId) returns (bool) // Get pending request IDs array function getPendingRequestIds() returns (uint256[] memory); +// Get immutable CREATE_YIELDVAULT config by ID +function getCreateYieldVaultConfig(uint64 configId) + returns (bool exists, bool enabled, string memory vaultIdentifier, string memory strategyIdentifier); + // Get pending requests in unpacked arrays (pagination) -function getPendingRequestsUnpacked(uint256 startIndex, uint256 count) returns (...); +function getPendingRequestsUnpacked(uint256 startIndex, uint256 count) + returns ( + uint256[] memory ids, + address[] memory users, + uint8[] memory requestTypes, + uint8[] memory statuses, + address[] memory tokenAddresses, + uint256[] memory amounts, + uint64[] memory yieldVaultIds, + uint256[] memory timestamps, + string[] memory messages, + uint64[] memory createVaultConfigIds + ); // Get pending requests for a user in unpacked arrays (includes native FLOW balances) -function getPendingRequestsByUserUnpacked(address user) returns (...); - -// Get single request by ID -function getRequest(uint256 requestId) returns (Request memory); +function getPendingRequestsByUserUnpacked(address user) + returns ( + uint256[] memory ids, + uint8[] memory requestTypes, + uint8[] memory statuses, + address[] memory tokenAddresses, + uint256[] memory amounts, + uint64[] memory yieldVaultIds, + uint256[] memory timestamps, + string[] memory messages, + uint64[] memory createVaultConfigIds, + uint256 pendingBalance, + uint256 claimableRefund + ); + +// Get single request by ID (resolved read model) +function getRequest(uint256 requestId) returns (RequestView memory); // Check if YieldVault Id is valid function isYieldVaultIdValid(uint64 yieldVaultId) returns (bool); diff --git a/FRONTEND_INTEGRATION.md b/FRONTEND_INTEGRATION.md index 97026e7..d73184a 100644 --- a/FRONTEND_INTEGRATION.md +++ b/FRONTEND_INTEGRATION.md @@ -162,7 +162,7 @@ Note: counts include both `PENDING` and `PROCESSING` requests (they remain in th ```typescript const request = await contract.getRequest(requestId); -// Returns: { id, user, requestType, status, tokenAddress, amount, yieldVaultId, timestamp, message, vaultIdentifier, strategyIdentifier } +// Returns: { id, user, requestType, status, tokenAddress, amount, yieldVaultId, timestamp, message, createVaultConfigId, vaultIdentifier, strategyIdentifier } ``` #### Get User's Pending Requests (Unpacked) @@ -177,14 +177,14 @@ const [ yieldVaultIds, timestamps, messages, - vaultIdentifiers, - strategyIdentifiers, + createVaultConfigIds, pendingBalance, claimableRefund, ] = await contract.getPendingRequestsByUserUnpacked(userAddress); // pendingBalance = escrowed funds for active pending requests (native FLOW only) // claimableRefund = funds available to claim via claimRefund() (native FLOW only) // Use getUserPendingBalance/getClaimableRefund for a specific token +// Use getCreateYieldVaultConfig(createVaultConfigIds[i]) to resolve vault/strategy identifiers for CREATE requests ``` #### Get All Pending Requests (Paginated, Admin) @@ -200,14 +200,14 @@ const [ yieldVaultIds, timestamps, messages, - vaultIdentifiers, - strategyIdentifiers, + createVaultConfigIds, ] = await contract.getPendingRequestsUnpacked(startIndex, count); // Filter for specific user client-side const userRequests = ids.filter( (_, i) => users[i].toLowerCase() === userAddress.toLowerCase() ); +// Use getCreateYieldVaultConfig(createVaultConfigIds[i]) to resolve vault/strategy identifiers for CREATE requests ``` --- diff --git a/README.md b/README.md index a1c0e63..1e36284 100644 --- a/README.md +++ b/README.md @@ -104,18 +104,19 @@ Recommended sequence (run from repo root): Notes: - These scripts expect `flow`, `forge`, `cast`, `curl`, `bc`, `lsof`, and `git` on PATH. - `./local/deploy_full_stack.sh` writes the deployed EVM contract address to `./local/.deployed_contract_address`. +- `./local/deploy_full_stack.sh` also registers the default local `CREATE_YIELDVAULT` config as ID `1`. - The E2E scripts read `./local/.deployed_contract_address` or use `FLOW_VAULTS_REQUESTS_CONTRACT` if set. Local script reference: - `./local/setup_and_run_emulator.sh`: Initializes submodules, clears `./db` and `./imports`, kills processes on ports 8080/8545/3569/8888, starts Flow emulator + EVM gateway, and sets up FlowYieldVaults dependencies. -- `./local/deploy_full_stack.sh`: Funds local EVM EOAs, deploys `FlowYieldVaultsRequests` to the local EVM, deploys Cadence contracts, sets up the Worker, and writes `./local/.deployed_contract_address`. +- `./local/deploy_full_stack.sh`: Funds local EVM EOAs, deploys `FlowYieldVaultsRequests` to the local EVM, deploys Cadence contracts, sets up the Worker, registers the default local `CREATE_YIELDVAULT` config, and writes `./local/.deployed_contract_address`. - `./local/run_e2e_tests.sh`: Runs end-to-end user flows (create/deposit/withdraw/close/cancel). Requires emulator/gateway running and a deployed contract address. - `./local/run_admin_e2e_tests.sh`: Runs end-to-end admin flows (allowlist/blocklist, token config, max requests, admin cancel/drop). Requires emulator/gateway running and a deployed contract address. - `./local/run_worker_tests.sh`: Runs scheduled worker tests (SchedulerHandler, WorkerHandler, pause/unpause, crash recovery). Requires emulator/gateway running and a deployed contract address. - `./local/run_cadence_tests.sh`: Runs Cadence tests with `flow test`. Cleans `./db` and `./imports` first (stop emulator if you need to preserve state). - `./local/run_solidity_tests.sh`: Runs Solidity tests with `forge test`. - `./local/testnet-e2e.sh`: Testnet CLI for state checks and user/admin actions. Run `./local/testnet-e2e.sh --help` for commands. Uses `PRIVATE_KEY` and `TESTNET_RPC_URL` if set; admin commands require `testnet-account` in `flow.json`. Update the hardcoded `CONTRACT` address in the script when deploying a new version. -- `./local/deploy_and_verify.sh`: Testnet deploy/verify flow using COA and KMS. Requires a `.env` file (see the script header for required values) and a configured `testnet-account` signer. +- `./local/deploy_and_verify.sh`: Testnet deploy/verify flow using COA and KMS. Requires a `.env` file, a configured `testnet-account` signer, and an initial `CREATE_YIELDVAULT` config via `INITIAL_CREATE_VAULT_CONFIG_ID`, `INITIAL_CREATE_VAULT_IDENTIFIER`, and `INITIAL_CREATE_STRATEGY_IDENTIFIER` unless you explicitly skip registration. Testnet sequence (optional): 1. Create `.env` with the variables expected by `./local/deploy_and_verify.sh` (KMS/signing config, RPCs, etc). @@ -132,8 +133,7 @@ All user operations are available through `FlowYieldVaultsYieldVaultOperations.s |----------|-------------|---------| | `USER_PRIVATE_KEY` | Private key for signing transactions | `0x3` (test account) | | `AMOUNT` | Amount in wei for create/deposit operations | `10000000000000000000` (10 FLOW) | -| `VAULT_IDENTIFIER` | Cadence vault type identifier | `A.0ae53cb6e3f42a79.FlowToken.Vault` | -| `STRATEGY_IDENTIFIER` | Cadence strategy type identifier | `A.045a1763c93006ca.MockStrategies.TracerStrategy` | +| `CREATE_VAULT_CONFIG_ID` | Registered `CREATE_YIELDVAULT` config ID | `1` | #### Commands @@ -144,8 +144,8 @@ forge script ./solidity/script/FlowYieldVaultsYieldVaultOperations.s.sol:FlowYie --sig "createYieldVault(address)" $FLOW_VAULTS_REQUESTS_CONTRACT \ --rpc-url http://localhost:8545 --broadcast --legacy -# CREATE_YIELDVAULT - Custom amount (100 FLOW) with custom signer -USER_PRIVATE_KEY=0xYOUR_PRIVATE_KEY AMOUNT=100000000000000000000 \ +# CREATE_YIELDVAULT - Custom amount (100 FLOW) and config ID with custom signer +USER_PRIVATE_KEY=0xYOUR_PRIVATE_KEY AMOUNT=100000000000000000000 CREATE_VAULT_CONFIG_ID=2 \ forge script ./solidity/script/FlowYieldVaultsYieldVaultOperations.s.sol:FlowYieldVaultsYieldVaultOperations \ --root ./solidity \ --sig "createYieldVault(address)" $FLOW_VAULTS_REQUESTS_CONTRACT \ diff --git a/cadence/contracts/FlowYieldVaultsEVM.cdc b/cadence/contracts/FlowYieldVaultsEVM.cdc index 5611b57..f3bb7d4 100644 --- a/cadence/contracts/FlowYieldVaultsEVM.cdc +++ b/cadence/contracts/FlowYieldVaultsEVM.cdc @@ -49,6 +49,19 @@ access(all) contract FlowYieldVaultsEVM { access(all) case FAILED } + /// @notice Immutable CREATE_YIELDVAULT configuration registered by admin + access(all) struct CreateYieldVaultConfig { + access(all) let id: UInt64 + access(all) let vaultIdentifier: String + access(all) let strategyIdentifier: String + + init(id: UInt64, vaultIdentifier: String, strategyIdentifier: String) { + self.id = id + self.vaultIdentifier = vaultIdentifier + self.strategyIdentifier = strategyIdentifier + } + } + /// @notice Decoded request data from EVM contract /// @dev Mirrors the Request struct in FlowYieldVaultsRequests.sol for cross-VM communication access(all) struct EVMRequest { @@ -61,6 +74,7 @@ access(all) contract FlowYieldVaultsEVM { access(all) let yieldVaultId: UInt64? access(all) let timestamp: UInt256 access(all) let message: String + access(all) let createVaultConfigId: UInt64? access(all) let vaultIdentifier: String access(all) let strategyIdentifier: String @@ -74,6 +88,7 @@ access(all) contract FlowYieldVaultsEVM { yieldVaultId: UInt64?, timestamp: UInt256, message: String, + createVaultConfigId: UInt64?, vaultIdentifier: String, strategyIdentifier: String ) { @@ -109,6 +124,11 @@ access(all) contract FlowYieldVaultsEVM { } self.timestamp = timestamp self.message = message + if createVaultConfigId == nil || createVaultConfigId! == 0 { + self.createVaultConfigId = nil + } else { + self.createVaultConfigId = createVaultConfigId + } self.vaultIdentifier = vaultIdentifier self.strategyIdentifier = strategyIdentifier } @@ -164,6 +184,12 @@ access(all) contract FlowYieldVaultsEVM { /// @dev Maps EVM address string to {yieldVaultId: true} for fast ownership checks access(all) let yieldVaultRegistry: {String: {UInt64: Bool}} + /// @notice Immutable CREATE_YIELDVAULT configs keyed by config ID + access(all) let createYieldVaultConfigs: {UInt64: CreateYieldVaultConfig} + + /// @notice Reverse index from vault/strategy pair to config ID + access(contract) let createYieldVaultConfigIdsByPairKey: {String: UInt64} + /// @notice Address of the FlowYieldVaultsRequests contract on EVM access(contract) var flowYieldVaultsRequestsAddress: EVM.EVMAddress? @@ -333,6 +359,13 @@ access(all) contract FlowYieldVaultsEVM { /// @param requestId The request ID that was cancelled access(all) event EVMRequestCancelled(requestId: UInt256) + /// @notice Emitted when a CREATE_YIELDVAULT config is successfully registered on the EVM contract + access(all) event EVMCreateYieldVaultConfigRegistered( + configId: UInt64, + vaultIdentifier: String, + strategyIdentifier: String + ) + // ============================================ // Resources // ============================================ @@ -359,6 +392,62 @@ access(all) contract FlowYieldVaultsEVM { emit FlowYieldVaultsRequestsAddressSet(address: address.toString()) } + /// @notice Registers an immutable CREATE_YIELDVAULT config locally on Cadence + /// @dev Contract-internal only. External callers must use FlowYieldVaultsEVM.registerCreateYieldVaultConfigEverywhere(). + /// @param configId Immutable config ID + /// @param vaultIdentifier Cadence vault type identifier + /// @param strategyIdentifier Cadence strategy type identifier + access(contract) fun registerCreateYieldVaultConfig( + configId: UInt64, + vaultIdentifier: String, + strategyIdentifier: String + ) { + pre { + configId > 0: "configId must be greater than 0" + FlowYieldVaultsEVM.createYieldVaultConfigs[configId] == nil: + "CreateYieldVault config \(configId) already registered" + FlowYieldVaultsEVM.createYieldVaultConfigIdsByPairKey[ + FlowYieldVaultsEVM.createYieldVaultConfigPairKey( + vaultIdentifier: vaultIdentifier, + strategyIdentifier: strategyIdentifier + ) + ] == nil: "CreateYieldVault config already registered for pair \(vaultIdentifier) / \(strategyIdentifier)" + } + + let vaultType = CompositeType(vaultIdentifier) + ?? panic("Invalid vaultIdentifier: \(vaultIdentifier)") + let strategyType = CompositeType(strategyIdentifier) + ?? panic("Invalid strategyIdentifier: \(strategyIdentifier)") + + let supportedStrategies = FlowYieldVaults.getSupportedStrategies() + var isStrategySupported = false + for supported in supportedStrategies { + if supported == strategyType { + isStrategySupported = true + break + } + } + if !isStrategySupported { + panic("Unsupported strategyIdentifier: \(strategyIdentifier)") + } + + let supportedVaults = FlowYieldVaults.getSupportedInitializationVaults(forStrategy: strategyType) + if supportedVaults[vaultType] != true { + panic("Unsupported vaultIdentifier \(vaultIdentifier) for strategy \(strategyIdentifier)") + } + + let pairKey = FlowYieldVaultsEVM.createYieldVaultConfigPairKey( + vaultIdentifier: vaultIdentifier, + strategyIdentifier: strategyIdentifier + ) + FlowYieldVaultsEVM.createYieldVaultConfigIdsByPairKey[pairKey] = configId + FlowYieldVaultsEVM.createYieldVaultConfigs[configId] = CreateYieldVaultConfig( + id: configId, + vaultIdentifier: vaultIdentifier, + strategyIdentifier: strategyIdentifier + ) + } + /// @notice Creates a new Worker resource /// @param coaCap Capability to the COA with Call, Withdraw, and Bridge entitlements /// @param yieldVaultManagerCap Capability to the YieldVaultManager with Withdraw entitlement @@ -461,7 +550,7 @@ access(all) contract FlowYieldVaultsEVM { /// @dev Flow: /// - Validate status - should be PENDING /// - Validate amount - should already be validated by Solidity, but check defensively - /// - Early validation for CREATE_YIELDVAULT requests - validate vaultIdentifier and strategyIdentifier + /// - Early validation for CREATE_YIELDVAULT requests - config ID must be registered locally /// - Call startProcessingBatch to update the request statuses (PENDING -> PROCESSING/FAILED) /// - Return successful requests for further processing /// @param requests The list of EVM requests to preprocess @@ -491,9 +580,8 @@ access(all) contract FlowYieldVaultsEVM { } // Early validation for CREATE_YIELDVAULT requests - // Validate vaultIdentifier and strategyIdentifier if request.requestType == FlowYieldVaultsEVM.RequestType.CREATE_YIELDVAULT.rawValue { - let validationResult = FlowYieldVaultsEVM.validateCreateYieldVaultParameters(request) + let validationResult = FlowYieldVaultsEVM.validateCreateYieldVaultConfig(request) if !validationResult.success { FlowYieldVaultsEVM.emitRequestFailed(request, message: "Validation failed: \(validationResult.message)") @@ -514,6 +602,7 @@ access(all) contract FlowYieldVaultsEVM { yieldVaultId: request.yieldVaultId, timestamp: request.timestamp, message: request.message, + createVaultConfigId: request.createVaultConfigId, vaultIdentifier: request.vaultIdentifier, strategyIdentifier: request.strategyIdentifier, ) @@ -715,11 +804,28 @@ access(all) contract FlowYieldVaultsEVM { /// 2. Validates vault type matches the requested vaultIdentifier /// 3. Creates YieldVault via YieldVaultManager /// 4. Records ownership in yieldVaultRegistry - /// @param request The CREATE_YIELDVAULT request containing vault/strategy identifiers and amount + /// @param request The CREATE_YIELDVAULT request containing the registered config ID and amount /// @return ProcessResult with success status, created yieldVaultId, and status message access(self) fun processCreateYieldVault(_ request: EVMRequest): ProcessResult { - let vaultIdentifier = request.vaultIdentifier - let strategyIdentifier = request.strategyIdentifier + if request.createVaultConfigId == nil { + return ProcessResult( + success: false, + yieldVaultId: nil, + message: "Missing createVaultConfigId for CREATE_YIELDVAULT request \(request.id)" + ) + } + + let config = FlowYieldVaultsEVM.getCreateYieldVaultConfig(request.createVaultConfigId!) + if config == nil { + return ProcessResult( + success: false, + yieldVaultId: nil, + message: "Unknown createVaultConfigId \(request.createVaultConfigId!) for request \(request.id)" + ) + } + + let vaultIdentifier = config!.vaultIdentifier + let strategyIdentifier = config!.strategyIdentifier let amount = FlowYieldVaultsEVM.ufix64FromUInt256(request.amount, tokenAddress: request.tokenAddress) // Phase 1: Withdraw funds from COA (bridges ERC20 to Cadence vault if needed) @@ -750,15 +856,21 @@ access(all) contract FlowYieldVaultsEVM { } // Phase 3: Create the YieldVault with the specified strategy - // Note: strategyIdentifier already validated by validateCreateYieldVaultParameters - let strategyType = CompositeType(strategyIdentifier)! + let strategyType = CompositeType(strategyIdentifier) + if strategyType == nil { + return self.returnFundsToCOAAndFail( + vault: <-vault, + tokenAddress: request.tokenAddress, + errorMessage: "Strategy type for config \(request.createVaultConfigId!) is no longer valid: \(strategyIdentifier)" + ) + } let betaRef = self.getBetaRef() let yieldVaultManager = self.getYieldVaultManagerRef() let yieldVaultId = yieldVaultManager.createYieldVault( betaRef: betaRef, - strategyType: strategyType, + strategyType: strategyType!, withVault: <-vault ) @@ -1290,8 +1402,7 @@ access(all) contract FlowYieldVaultsEVM { Type<[UInt64]>(), Type<[UInt256]>(), Type<[String]>(), - Type<[String]>(), - Type<[String]>() + Type<[UInt64]>() ], data: callResult.data ) @@ -1305,12 +1416,15 @@ access(all) contract FlowYieldVaultsEVM { let yieldVaultIds = decoded[6] as! [UInt64] let timestamps = decoded[7] as! [UInt256] let messages = decoded[8] as! [String] - let vaultIdentifiers = decoded[9] as! [String] - let strategyIdentifiers = decoded[10] as! [String] + let createVaultConfigIds = decoded[9] as! [UInt64] let requests: [EVMRequest] = [] var i = 0 while i < ids.length { + let createVaultConfigId = createVaultConfigIds[i] + let config = FlowYieldVaultsEVM.getCreateYieldVaultConfig(createVaultConfigId) + let vaultIdentifier = config?.vaultIdentifier ?? "" + let strategyIdentifier = config?.strategyIdentifier ?? "" let request = EVMRequest( id: ids[i], user: users[i], @@ -1321,8 +1435,9 @@ access(all) contract FlowYieldVaultsEVM { yieldVaultId: yieldVaultIds[i], timestamp: timestamps[i], message: messages[i], - vaultIdentifier: vaultIdentifiers[i], - strategyIdentifier: strategyIdentifiers[i] + createVaultConfigId: createVaultConfigId, + vaultIdentifier: vaultIdentifier, + strategyIdentifier: strategyIdentifier ) requests.append(request) i = i + 1 @@ -1511,6 +1626,46 @@ access(all) contract FlowYieldVaultsEVM { ) } + /// @notice Registers an immutable CREATE_YIELDVAULT config on the EVM request contract + /// @dev Contract-internal only. External callers must use FlowYieldVaultsEVM.registerCreateYieldVaultConfigEverywhere(). + /// If the EVM call fails, the surrounding Cadence transaction should revert and roll back local writes. + /// @param configId Immutable config ID shared with the EVM request contract + /// @param vaultIdentifier Cadence vault type identifier + /// @param strategyIdentifier Cadence strategy type identifier + access(contract) fun registerCreateYieldVaultConfigOnEVM( + configId: UInt64, + vaultIdentifier: String, + strategyIdentifier: String + ) { + pre { + FlowYieldVaultsEVM.flowYieldVaultsRequestsAddress != nil: + "FlowYieldVaultsRequests address not set - call Admin.setFlowYieldVaultsRequestsAddress() first" + } + + let calldata = EVM.encodeABIWithSignature( + "registerCreateYieldVaultConfig(uint64,string,string)", + [configId, vaultIdentifier, strategyIdentifier] + ) + + let result = self.getCOARef().call( + to: FlowYieldVaultsEVM.flowYieldVaultsRequestsAddress!, + data: calldata, + gasLimit: 300_000, + value: EVM.Balance(attoflow: 0) + ) + + if result.status != EVM.Status.successful { + let errorMsg = FlowYieldVaultsEVM.decodeEVMError(result.data) + panic("registerCreateYieldVaultConfigOnEVM failed: \(errorMsg)") + } + + emit EVMCreateYieldVaultConfigRegistered( + configId: configId, + vaultIdentifier: vaultIdentifier, + strategyIdentifier: strategyIdentifier + ) + } + /// @notice Sets the authorized COA address on the EVM contract /// @param coa The new authorized COA address access(all) fun setAuthorizedCOA(_ coa: EVM.EVMAddress) { @@ -1640,6 +1795,57 @@ access(all) contract FlowYieldVaultsEVM { return self.flowYieldVaultsRequestsAddress } + /// @notice Registers an immutable CREATE_YIELDVAULT config on both Cadence and the EVM request contract + /// @dev This is the only public config-registration entrypoint. It validates and stores the local Cadence config + /// first, then performs the COA owner call on EVM. If the EVM call fails, the surrounding Cadence transaction + /// reverts and the local write is rolled back. + /// @param admin Borrowed Admin resource from the signer account + /// @param worker Borrowed Worker resource from the signer account + /// @param configId Immutable config ID shared with the EVM request contract + /// @param vaultIdentifier Cadence vault type identifier + /// @param strategyIdentifier Cadence strategy type identifier + access(all) fun registerCreateYieldVaultConfigEverywhere( + admin: &Admin, + worker: &Worker, + configId: UInt64, + vaultIdentifier: String, + strategyIdentifier: String + ) { + admin.registerCreateYieldVaultConfig( + configId: configId, + vaultIdentifier: vaultIdentifier, + strategyIdentifier: strategyIdentifier + ) + + worker.registerCreateYieldVaultConfigOnEVM( + configId: configId, + vaultIdentifier: vaultIdentifier, + strategyIdentifier: strategyIdentifier + ) + } + + /// @notice Gets a registered CREATE_YIELDVAULT config by ID + /// @param configId Immutable config ID + /// @return The registered config or nil if not found + access(all) view fun getCreateYieldVaultConfig(_ configId: UInt64): CreateYieldVaultConfig? { + return self.createYieldVaultConfigs[configId] + } + + /// @notice Gets the registered CREATE_YIELDVAULT config ID for a vault/strategy pair + /// @param vaultIdentifier Cadence vault type identifier + /// @param strategyIdentifier Cadence strategy type identifier + /// @return The registered config ID or nil if not found + access(all) view fun getCreateYieldVaultConfigId( + vaultIdentifier: String, + strategyIdentifier: String + ): UInt64? { + let pairKey = self.createYieldVaultConfigPairKey( + vaultIdentifier: vaultIdentifier, + strategyIdentifier: strategyIdentifier + ) + return self.createYieldVaultConfigIdsByPairKey[pairKey] + } + /// @notice Gets pending requests for a specific EVM address (public query) /// @dev Uses the contract account's public COA capability at /public/evm for read-only EVM calls. /// @param evmAddressHex The EVM address as a hex string (e.g., "0x1234...") @@ -1680,8 +1886,7 @@ access(all) contract FlowYieldVaultsEVM { Type<[UInt64]>(), // yieldVaultIds Type<[UInt256]>(), // timestamps Type<[String]>(), // messages - Type<[String]>(), // vaultIdentifiers - Type<[String]>(), // strategyIdentifiers + Type<[UInt64]>(), // createVaultConfigIds Type(), // pendingBalance Type() // claimableRefund ], @@ -1696,10 +1901,9 @@ access(all) contract FlowYieldVaultsEVM { let yieldVaultIds = decoded[5] as! [UInt64] let timestamps = decoded[6] as! [UInt256] let messages = decoded[7] as! [String] - let vaultIdentifiers = decoded[8] as! [String] - let strategyIdentifiers = decoded[9] as! [String] - let pendingBalanceRaw = decoded[10] as! UInt256 - let claimableRefundRaw = decoded[11] as! UInt256 + let createVaultConfigIds = decoded[8] as! [UInt64] + let pendingBalanceRaw = decoded[9] as! UInt256 + let claimableRefundRaw = decoded[10] as! UInt256 // Convert pending balance from wei to UFix64 let pendingBalance = FlowEVMBridgeUtils.uint256ToUFix64(value: pendingBalanceRaw, decimals: 18) @@ -1709,6 +1913,10 @@ access(all) contract FlowYieldVaultsEVM { var requests: [EVMRequest] = [] var i = 0 while i < ids.length { + let createVaultConfigId = createVaultConfigIds[i] + let config = FlowYieldVaultsEVM.getCreateYieldVaultConfig(createVaultConfigId) + let vaultIdentifier = config?.vaultIdentifier ?? "" + let strategyIdentifier = config?.strategyIdentifier ?? "" let request = EVMRequest( id: ids[i], user: evmAddress, @@ -1719,8 +1927,9 @@ access(all) contract FlowYieldVaultsEVM { yieldVaultId: yieldVaultIds[i], timestamp: timestamps[i], message: messages[i], - vaultIdentifier: vaultIdentifiers[i], - strategyIdentifier: strategyIdentifiers[i] + createVaultConfigId: createVaultConfigId, + vaultIdentifier: vaultIdentifier, + strategyIdentifier: strategyIdentifier ) requests.append(request) i = i + 1 @@ -1776,8 +1985,7 @@ access(all) contract FlowYieldVaultsEVM { Type(), // yieldVaultId Type(), // timestamp Type(), // message - Type(), // vaultIdentifier - Type() // strategyIdentifier + Type() // createVaultConfigId ], data: callResult.data ) @@ -1791,14 +1999,17 @@ access(all) contract FlowYieldVaultsEVM { let yieldVaultId = decoded[6] as! UInt64 let timestamp = decoded[7] as! UInt256 let message = decoded[8] as! String - let vaultIdentifier = decoded[9] as! String - let strategyIdentifier = decoded[10] as! String + let createVaultConfigId = decoded[9] as! UInt64 // Request not found if timestamp == 0 { return nil } + let config = FlowYieldVaultsEVM.getCreateYieldVaultConfig(createVaultConfigId) + let vaultIdentifier = config?.vaultIdentifier ?? "" + let strategyIdentifier = config?.strategyIdentifier ?? "" + // Build request array let request = EVMRequest( id: id, @@ -1810,6 +2021,7 @@ access(all) contract FlowYieldVaultsEVM { yieldVaultId: yieldVaultId, timestamp: timestamp, message: message, + createVaultConfigId: createVaultConfigId, vaultIdentifier: vaultIdentifier, strategyIdentifier: strategyIdentifier ) @@ -1854,66 +2066,32 @@ access(all) contract FlowYieldVaultsEVM { // Internal Functions // ============================================ - /// @notice Validates CREATE_YIELDVAULT request parameters before processing - /// @dev Validates that vaultIdentifier and strategyIdentifier are valid Cadence types - /// and that the strategy is supported by FlowYieldVaults protocol. - /// This prevents panics during createYieldVault by catching invalid parameters early. - /// Note: Basic string validations (empty checks) should be done on the Solidity side. + /// @notice Validates CREATE_YIELDVAULT request config presence before processing + /// @dev Scheduler paths only check that the immutable config ID is registered locally. + /// Dynamic Cadence type resolution happens only in admin registration or worker execution paths. /// @param request The request to validate /// @return ProcessResult with success=true if valid, or success=false with error message - access(self) fun validateCreateYieldVaultParameters(_ request: EVMRequest): ProcessResult { - // Validate vaultIdentifier is a valid Cadence type identifier - let vaultType = CompositeType(request.vaultIdentifier) - if vaultType == nil { + access(self) fun validateCreateYieldVaultConfig(_ request: EVMRequest): ProcessResult { + if request.createVaultConfigId == nil { return ProcessResult( success: false, yieldVaultId: nil, - message: "Invalid vaultIdentifier: \(request.vaultIdentifier) is not a valid Cadence type" + message: "Missing createVaultConfigId for CREATE_YIELDVAULT request \(request.id)" ) } - // Validate strategyIdentifier is a valid Cadence type identifier - let strategyType = CompositeType(request.strategyIdentifier) - if strategyType == nil { + if FlowYieldVaultsEVM.getCreateYieldVaultConfig(request.createVaultConfigId!) == nil { return ProcessResult( success: false, yieldVaultId: nil, - message: "Invalid strategyIdentifier: \(request.strategyIdentifier) is not a valid Cadence type" + message: "Unknown createVaultConfigId \(request.createVaultConfigId!)" ) } - // Validate strategy is supported by FlowYieldVaults protocol - let supportedStrategies = FlowYieldVaults.getSupportedStrategies() - var isStrategySupported = false - for supported in supportedStrategies { - if supported == strategyType! { - isStrategySupported = true - break - } - } - if !isStrategySupported { - return ProcessResult( - success: false, - yieldVaultId: nil, - message: "Unsupported strategy: \(request.strategyIdentifier) is not supported by FlowYieldVaults" - ) - } - - // Validate vault type is supported for this strategy's initialization - let supportedVaults = FlowYieldVaults.getSupportedInitializationVaults(forStrategy: strategyType!) - if supportedVaults[vaultType!] != true { - return ProcessResult( - success: false, - yieldVaultId: nil, - message: "Unsupported vault type: \(request.vaultIdentifier) cannot be used to initialize strategy \(request.strategyIdentifier)" - ) - } - - // Validation passed return ProcessResult( success: true, yieldVaultId: nil, - message: "Validation passed" + message: "CreateYieldVault config registered" ) } @@ -1990,6 +2168,15 @@ access(all) contract FlowYieldVaultsEVM { return "EVM revert data: 0x\(String.encodeHex(data))" } + /// @notice Creates a stable dictionary key for a vault/strategy config pair + /// @dev Length prefixes avoid collisions between concatenated identifiers. + access(contract) view fun createYieldVaultConfigPairKey( + vaultIdentifier: String, + strategyIdentifier: String + ): String { + return "\(vaultIdentifier.length):\(vaultIdentifier)|\(strategyIdentifier.length):\(strategyIdentifier)" + } + /// @notice Emits the RequestFailed event and returns a ProcessResult with success=false /// @dev This is a helper function to emit the RequestFailed event and return a ProcessResult with success=false /// @param request The EVM request that failed @@ -2035,6 +2222,8 @@ access(all) contract FlowYieldVaultsEVM { self.WorkerStoragePath = /storage/flowYieldVaultsEVM self.AdminStoragePath = /storage/flowYieldVaultsEVMAdmin self.yieldVaultRegistry = {} + self.createYieldVaultConfigs = {} + self.createYieldVaultConfigIdsByPairKey = {} self.flowYieldVaultsRequestsAddress = nil let admin <- create Admin() diff --git a/cadence/tests/access_control_test.cdc b/cadence/tests/access_control_test.cdc index 56c4b9e..380581e 100644 --- a/cadence/tests/access_control_test.cdc +++ b/cadence/tests/access_control_test.cdc @@ -109,4 +109,4 @@ fun testYieldVaultRegistryMapping() { // Should return empty array for address with no yieldvaults Test.assertEqual(0, yieldVaultIds.length) -} \ No newline at end of file +} diff --git a/cadence/tests/config_registration_test.cdc b/cadence/tests/config_registration_test.cdc new file mode 100644 index 0000000..73858ff --- /dev/null +++ b/cadence/tests/config_registration_test.cdc @@ -0,0 +1,76 @@ +import Test +import "FlowYieldVaultsEVM" +import "test_helpers.cdc" + +// ----------------------------------------------------------------------------- +// Config Registration Test +// ----------------------------------------------------------------------------- +// Tests cross-VM admin registration flows for CREATE_YIELDVAULT configs. +// ----------------------------------------------------------------------------- + +access(all) +fun setup() { + deployContracts() + + let coaResult = setupCOA(admin) + Test.expect(coaResult, Test.beSucceeded()) + + let workerResult = setupWorkerWithBadge(admin) + Test.expect(workerResult, Test.beSucceeded()) + + let setAddrResult = updateRequestsAddress(admin, mockRequestsAddr.toString()) + Test.expect(setAddrResult, Test.beSucceeded()) +} + +access(all) +fun testRegisterCreateYieldVaultConfigEverywhere_RollsBackOnEVMFailure() { + Test.assert( + FlowYieldVaultsEVM.getCreateYieldVaultConfig(mockCreateVaultConfigId) == nil, + message: "Config should not exist before registration" + ) + + let registerResult = registerCreateYieldVaultConfigEverywhere( + admin, + mockCreateVaultConfigId, + mockVaultIdentifier, + mockStrategyIdentifier + ) + Test.assertEqual(Test.ResultStatus.failed, registerResult.status) + + Test.assert( + FlowYieldVaultsEVM.getCreateYieldVaultConfig(mockCreateVaultConfigId) == nil, + message: "Cadence config should roll back if the EVM registration fails" + ) +} + +access(all) +fun testDirectCadenceOnlyConfigRegistrationIsForbidden() { + let registerResult = registerCreateYieldVaultConfigDirectAdminForTest( + admin, + mockCreateVaultConfigId, + mockVaultIdentifier, + mockStrategyIdentifier + ) + Test.assertEqual(Test.ResultStatus.failed, registerResult.status) + + Test.assert( + FlowYieldVaultsEVM.getCreateYieldVaultConfig(mockCreateVaultConfigId) == nil, + message: "Cadence config should remain unset when direct Admin registration is attempted" + ) +} + +access(all) +fun testDirectEVMOnlyConfigRegistrationIsForbidden() { + let registerResult = registerCreateYieldVaultConfigDirectWorkerForTest( + admin, + mockCreateVaultConfigId, + mockVaultIdentifier, + mockStrategyIdentifier + ) + Test.assertEqual(Test.ResultStatus.failed, registerResult.status) + + Test.assert( + FlowYieldVaultsEVM.getCreateYieldVaultConfig(mockCreateVaultConfigId) == nil, + message: "Cadence config should remain unset when direct Worker registration is attempted" + ) +} diff --git a/cadence/tests/error_handling_test.cdc b/cadence/tests/error_handling_test.cdc index 5ac3ad1..83f4dbc 100644 --- a/cadence/tests/error_handling_test.cdc +++ b/cadence/tests/error_handling_test.cdc @@ -49,9 +49,13 @@ fun testInvalidRequestType() { for requestType in validTypes { var amount = 1000000000000000000 as UInt256 + var createVaultConfigId: UInt64? = nil if requestType == FlowYieldVaultsEVM.RequestType.CLOSE_YIELDVAULT.rawValue { amount = 0 } + if requestType == FlowYieldVaultsEVM.RequestType.CREATE_YIELDVAULT.rawValue { + createVaultConfigId = mockCreateVaultConfigId + } let validRequest = FlowYieldVaultsEVM.EVMRequest( id: UInt256(requestType), @@ -63,6 +67,7 @@ fun testInvalidRequestType() { yieldVaultId: UInt64.max, timestamp: 0, message: "", + createVaultConfigId: createVaultConfigId, vaultIdentifier: mockVaultIdentifier, strategyIdentifier: mockStrategyIdentifier ) @@ -86,6 +91,7 @@ fun testInvalidRequestType() { yieldVaultId: 1, timestamp: 0, message: "", + createVaultConfigId: nil, vaultIdentifier: "", strategyIdentifier: "" ) @@ -106,6 +112,7 @@ fun testZeroAmountWithdrawal() { yieldVaultId: 1, timestamp: 0, message: "", + createVaultConfigId: nil, vaultIdentifier: "", strategyIdentifier: "" ) @@ -125,6 +132,7 @@ fun testZeroAmountWithdrawal() { yieldVaultId: 1, timestamp: 0, message: "", + createVaultConfigId: nil, vaultIdentifier: "", strategyIdentifier: "" ) @@ -148,6 +156,7 @@ fun testZeroAmountWithdrawal() { yieldVaultId: 1, timestamp: 0, message: "", + createVaultConfigId: nil, vaultIdentifier: "", strategyIdentifier: "" ) @@ -172,6 +181,7 @@ fun testRequestStatusCompletedStructure() { yieldVaultId: 1, timestamp: 0, message: "Successfully created", + createVaultConfigId: mockCreateVaultConfigId, vaultIdentifier: mockVaultIdentifier, strategyIdentifier: mockStrategyIdentifier ) @@ -193,6 +203,7 @@ fun testRequestStatusFailedStructure() { yieldVaultId: 1, timestamp: 0, message: "Insufficient balance", + createVaultConfigId: nil, vaultIdentifier: "", strategyIdentifier: "" ) diff --git a/cadence/tests/evm_bridge_lifecycle_test.cdc b/cadence/tests/evm_bridge_lifecycle_test.cdc index 238b525..c1f6f89 100644 --- a/cadence/tests/evm_bridge_lifecycle_test.cdc +++ b/cadence/tests/evm_bridge_lifecycle_test.cdc @@ -53,6 +53,7 @@ fun testCreateYieldVaultFromEVMRequest() { yieldVaultId: nil, timestamp: 0, message: "", + createVaultConfigId: mockCreateVaultConfigId, vaultIdentifier: mockVaultIdentifier, strategyIdentifier: mockStrategyIdentifier ) @@ -73,6 +74,7 @@ fun testCreateYieldVaultFromEVMRequest() { // --- assert ------------------------------------------------------------ // Verify the request structure is valid for processing Test.assert(createRequest.amount > 0, message: "Amount must be positive") + Test.assertEqual(mockCreateVaultConfigId, createRequest.createVaultConfigId!) Test.assertEqual(mockVaultIdentifier, createRequest.vaultIdentifier) Test.assertEqual(mockStrategyIdentifier, createRequest.strategyIdentifier) } @@ -91,6 +93,7 @@ fun testDepositToExistingYieldVault() { yieldVaultId: 1, timestamp: 0, message: "", + createVaultConfigId: nil, vaultIdentifier: "", // Not needed for DEPOSIT strategyIdentifier: "" ) @@ -115,6 +118,7 @@ fun testWithdrawFromYieldVault() { yieldVaultId: 1, timestamp: 0, message: "", + createVaultConfigId: nil, vaultIdentifier: "", strategyIdentifier: "" ) @@ -139,6 +143,7 @@ fun testCloseYieldVaultComplete() { yieldVaultId: 1, timestamp: 0, message: "", + createVaultConfigId: nil, vaultIdentifier: "", strategyIdentifier: "" ) @@ -164,6 +169,7 @@ fun testRequestStatusTransitions() { yieldVaultId: nil, timestamp: 0, message: "", + createVaultConfigId: mockCreateVaultConfigId, vaultIdentifier: mockVaultIdentifier, strategyIdentifier: mockStrategyIdentifier ) @@ -180,6 +186,7 @@ fun testRequestStatusTransitions() { yieldVaultId: nil, timestamp: 0, message: "Insufficient balance", + createVaultConfigId: mockCreateVaultConfigId, vaultIdentifier: mockVaultIdentifier, strategyIdentifier: mockStrategyIdentifier ) @@ -200,6 +207,7 @@ fun testMultipleUsersIndependentYieldVaults() { yieldVaultId: nil, timestamp: 0, message: "", + createVaultConfigId: mockCreateVaultConfigId, vaultIdentifier: mockVaultIdentifier, strategyIdentifier: mockStrategyIdentifier ) @@ -214,6 +222,7 @@ fun testMultipleUsersIndependentYieldVaults() { yieldVaultId: nil, timestamp: 0, message: "", + createVaultConfigId: mockCreateVaultConfigId, vaultIdentifier: mockVaultIdentifier, strategyIdentifier: mockStrategyIdentifier ) @@ -271,10 +280,12 @@ fun testVaultAndStrategyIdentifiers() { yieldVaultId: nil, timestamp: 0, message: "", + createVaultConfigId: mockCreateVaultConfigId, vaultIdentifier: customVaultId, strategyIdentifier: customStrategyId ) + Test.assertEqual(mockCreateVaultConfigId, request.createVaultConfigId!) Test.assertEqual(customVaultId, request.vaultIdentifier) Test.assertEqual(customStrategyId, request.strategyIdentifier) } diff --git a/cadence/tests/test_helpers.cdc b/cadence/tests/test_helpers.cdc index ff17511..3266e2f 100644 --- a/cadence/tests/test_helpers.cdc +++ b/cadence/tests/test_helpers.cdc @@ -17,6 +17,7 @@ access(all) let nativeFlowAddr = EVM.addressFromString("0xFFfFfFffFFfffFFfFFfFFF /* --- Mock Vault and Strategy Identifiers --- */ +access(all) let mockCreateVaultConfigId: UInt64 = 1 access(all) let mockVaultIdentifier = "A.0ae53cb6e3f42a79.FlowToken.Vault" access(all) let mockStrategyIdentifier = "A.045a1763c93006ca.MockStrategies.TracerStrategy" @@ -245,6 +246,48 @@ fun updateRequestsAddress(_ signer: Test.TestAccount, _ address: String): Test.T ) } +access(all) +fun registerCreateYieldVaultConfigEverywhere( + _ signer: Test.TestAccount, + _ configId: UInt64, + _ vaultIdentifier: String, + _ strategyIdentifier: String +): Test.TransactionResult { + return _executeTransaction( + "../transactions/register_create_yieldvault_config_everywhere.cdc", + [configId, vaultIdentifier, strategyIdentifier], + signer + ) +} + +access(all) +fun registerCreateYieldVaultConfigDirectAdminForTest( + _ signer: Test.TestAccount, + _ configId: UInt64, + _ vaultIdentifier: String, + _ strategyIdentifier: String +): Test.TransactionResult { + return _executeTransaction( + "transactions/register_create_yieldvault_config_direct_admin_forbidden.cdc", + [configId, vaultIdentifier, strategyIdentifier], + signer + ) +} + +access(all) +fun registerCreateYieldVaultConfigDirectWorkerForTest( + _ signer: Test.TestAccount, + _ configId: UInt64, + _ vaultIdentifier: String, + _ strategyIdentifier: String +): Test.TransactionResult { + return _executeTransaction( + "transactions/register_create_yieldvault_config_direct_worker_forbidden.cdc", + [configId, vaultIdentifier, strategyIdentifier], + signer + ) +} + access(all) fun setupWorkerWithBadge(_ admin: Test.TestAccount): Test.TransactionResult { return _executeTransaction( @@ -327,6 +370,7 @@ fun createEVMRequest( yieldVaultId: UInt64, timestamp: UInt256, message: String, + createVaultConfigId: UInt64?, vaultIdentifier: String, strategyIdentifier: String ): FlowYieldVaultsEVM.EVMRequest { @@ -340,6 +384,7 @@ fun createEVMRequest( yieldVaultId: yieldVaultId, timestamp: timestamp, message: message, + createVaultConfigId: createVaultConfigId, vaultIdentifier: vaultIdentifier, strategyIdentifier: strategyIdentifier ) diff --git a/cadence/tests/transactions/register_create_yieldvault_config_direct_admin_forbidden.cdc b/cadence/tests/transactions/register_create_yieldvault_config_direct_admin_forbidden.cdc new file mode 100644 index 0000000..7b1e879 --- /dev/null +++ b/cadence/tests/transactions/register_create_yieldvault_config_direct_admin_forbidden.cdc @@ -0,0 +1,15 @@ +import "FlowYieldVaultsEVM" + +transaction(configId: UInt64, vaultIdentifier: String, strategyIdentifier: String) { + prepare(signer: auth(BorrowValue) &Account) { + let admin = signer.storage.borrow<&FlowYieldVaultsEVM.Admin>( + from: FlowYieldVaultsEVM.AdminStoragePath + ) ?? panic("Could not borrow Admin resource") + + admin.registerCreateYieldVaultConfig( + configId: configId, + vaultIdentifier: vaultIdentifier, + strategyIdentifier: strategyIdentifier + ) + } +} diff --git a/cadence/tests/transactions/register_create_yieldvault_config_direct_worker_forbidden.cdc b/cadence/tests/transactions/register_create_yieldvault_config_direct_worker_forbidden.cdc new file mode 100644 index 0000000..8531b61 --- /dev/null +++ b/cadence/tests/transactions/register_create_yieldvault_config_direct_worker_forbidden.cdc @@ -0,0 +1,15 @@ +import "FlowYieldVaultsEVM" + +transaction(configId: UInt64, vaultIdentifier: String, strategyIdentifier: String) { + prepare(signer: auth(BorrowValue) &Account) { + let worker = signer.storage.borrow<&FlowYieldVaultsEVM.Worker>( + from: FlowYieldVaultsEVM.WorkerStoragePath + ) ?? panic("Could not borrow Worker resource") + + worker.registerCreateYieldVaultConfigOnEVM( + configId: configId, + vaultIdentifier: vaultIdentifier, + strategyIdentifier: strategyIdentifier + ) + } +} diff --git a/cadence/transactions/register_create_yieldvault_config_everywhere.cdc b/cadence/transactions/register_create_yieldvault_config_everywhere.cdc new file mode 100644 index 0000000..9b1d0a8 --- /dev/null +++ b/cadence/transactions/register_create_yieldvault_config_everywhere.cdc @@ -0,0 +1,30 @@ +import "FlowYieldVaultsEVM" + +/// @title Register Create YieldVault Config Everywhere +/// @notice Registers an immutable CREATE_YIELDVAULT config on both Cadence and the EVM request contract +/// @dev Borrows both Admin and Worker from the same signer account and delegates to the contract's only +/// public config-registration entrypoint. If the EVM call fails, the entire transaction reverts and +/// the local Cadence registration is rolled back. +/// +/// @param configId Immutable config ID shared with the EVM request contract +/// @param vaultIdentifier Cadence vault type identifier +/// @param strategyIdentifier Cadence strategy type identifier +transaction(configId: UInt64, vaultIdentifier: String, strategyIdentifier: String) { + prepare(signer: auth(BorrowValue) &Account) { + let admin = signer.storage.borrow<&FlowYieldVaultsEVM.Admin>( + from: FlowYieldVaultsEVM.AdminStoragePath + ) ?? panic("Could not borrow Admin resource") + + let worker = signer.storage.borrow<&FlowYieldVaultsEVM.Worker>( + from: FlowYieldVaultsEVM.WorkerStoragePath + ) ?? panic("Could not borrow Worker resource") + + FlowYieldVaultsEVM.registerCreateYieldVaultConfigEverywhere( + admin: admin, + worker: worker, + configId: configId, + vaultIdentifier: vaultIdentifier, + strategyIdentifier: strategyIdentifier + ) + } +} diff --git a/deployments/artifacts/FlowYieldVaultsRequests.json b/deployments/artifacts/FlowYieldVaultsRequests.json index 88f282c..f256c49 100644 --- a/deployments/artifacts/FlowYieldVaultsRequests.json +++ b/deployments/artifacts/FlowYieldVaultsRequests.json @@ -354,6 +354,54 @@ ], "stateMutability": "payable" }, + { + "type": "function", + "name": "createYieldVault", + "inputs": [ + { + "name": "tokenAddress", + "type": "address", + "internalType": "address" + }, + { + "name": "amount", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "createVaultConfigId", + "type": "uint64", + "internalType": "uint64" + } + ], + "outputs": [ + { + "name": "", + "type": "uint256", + "internalType": "uint256" + } + ], + "stateMutability": "payable" + }, + { + "type": "function", + "name": "createYieldVaultConfigIdByPairHash", + "inputs": [ + { + "name": "", + "type": "bytes32", + "internalType": "bytes32" + } + ], + "outputs": [ + { + "name": "", + "type": "uint64", + "internalType": "uint64" + } + ], + "stateMutability": "view" + }, { "type": "function", "name": "depositToYieldVault", @@ -444,6 +492,64 @@ ], "stateMutability": "view" }, + { + "type": "function", + "name": "getCreateYieldVaultConfig", + "inputs": [ + { + "name": "configId", + "type": "uint64", + "internalType": "uint64" + } + ], + "outputs": [ + { + "name": "exists", + "type": "bool", + "internalType": "bool" + }, + { + "name": "enabled", + "type": "bool", + "internalType": "bool" + }, + { + "name": "vaultIdentifier", + "type": "string", + "internalType": "string" + }, + { + "name": "strategyIdentifier", + "type": "string", + "internalType": "string" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "getCreateYieldVaultConfigId", + "inputs": [ + { + "name": "vaultIdentifier", + "type": "string", + "internalType": "string" + }, + { + "name": "strategyIdentifier", + "type": "string", + "internalType": "string" + } + ], + "outputs": [ + { + "name": "configId", + "type": "uint64", + "internalType": "uint64" + } + ], + "stateMutability": "view" + }, { "type": "function", "name": "getPendingRequestCount", @@ -522,14 +628,9 @@ "internalType": "string[]" }, { - "name": "vaultIdentifiers", - "type": "string[]", - "internalType": "string[]" - }, - { - "name": "strategyIdentifiers", - "type": "string[]", - "internalType": "string[]" + "name": "createVaultConfigIds", + "type": "uint64[]", + "internalType": "uint64[]" }, { "name": "pendingBalance", @@ -606,14 +707,9 @@ "internalType": "string[]" }, { - "name": "vaultIdentifiers", - "type": "string[]", - "internalType": "string[]" - }, - { - "name": "strategyIdentifiers", - "type": "string[]", - "internalType": "string[]" + "name": "createVaultConfigIds", + "type": "uint64[]", + "internalType": "uint64[]" } ], "stateMutability": "view" @@ -632,7 +728,7 @@ { "name": "", "type": "tuple", - "internalType": "struct FlowYieldVaultsRequests.Request", + "internalType": "struct FlowYieldVaultsRequests.RequestView", "components": [ { "name": "id", @@ -679,6 +775,11 @@ "type": "string", "internalType": "string" }, + { + "name": "createVaultConfigId", + "type": "uint64", + "internalType": "uint64" + }, { "name": "vaultIdentifier", "type": "string", @@ -751,14 +852,9 @@ "internalType": "string" }, { - "name": "vaultIdentifier", - "type": "string", - "internalType": "string" - }, - { - "name": "strategyIdentifier", - "type": "string", - "internalType": "string" + "name": "createVaultConfigId", + "type": "uint64", + "internalType": "uint64" } ], "stateMutability": "view" @@ -989,6 +1085,29 @@ ], "stateMutability": "view" }, + { + "type": "function", + "name": "registerCreateYieldVaultConfig", + "inputs": [ + { + "name": "configId", + "type": "uint64", + "internalType": "uint64" + }, + { + "name": "vaultIdentifier", + "type": "string", + "internalType": "string" + }, + { + "name": "strategyIdentifier", + "type": "string", + "internalType": "string" + } + ], + "outputs": [], + "stateMutability": "nonpayable" + }, { "type": "function", "name": "renounceOwnership", @@ -1053,14 +1172,9 @@ "internalType": "string" }, { - "name": "vaultIdentifier", - "type": "string", - "internalType": "string" - }, - { - "name": "strategyIdentifier", - "type": "string", - "internalType": "string" + "name": "createVaultConfigId", + "type": "uint64", + "internalType": "uint64" } ], "stateMutability": "view" @@ -1104,6 +1218,24 @@ "outputs": [], "stateMutability": "nonpayable" }, + { + "type": "function", + "name": "setCreateYieldVaultConfigEnabled", + "inputs": [ + { + "name": "configId", + "type": "uint64", + "internalType": "uint64" + }, + { + "name": "enabled", + "type": "bool", + "internalType": "bool" + } + ], + "outputs": [], + "stateMutability": "nonpayable" + }, { "type": "function", "name": "setMaxPendingRequestsPerUser", @@ -1489,6 +1621,62 @@ ], "anonymous": false }, + { + "type": "event", + "name": "CreateYieldVaultConfigEnabled", + "inputs": [ + { + "name": "configId", + "type": "uint64", + "indexed": true, + "internalType": "uint64" + }, + { + "name": "enabled", + "type": "bool", + "indexed": false, + "internalType": "bool" + }, + { + "name": "updatedBy", + "type": "address", + "indexed": true, + "internalType": "address" + } + ], + "anonymous": false + }, + { + "type": "event", + "name": "CreateYieldVaultConfigRegistered", + "inputs": [ + { + "name": "configId", + "type": "uint64", + "indexed": true, + "internalType": "uint64" + }, + { + "name": "vaultIdentifier", + "type": "string", + "indexed": false, + "internalType": "string" + }, + { + "name": "strategyIdentifier", + "type": "string", + "indexed": false, + "internalType": "string" + }, + { + "name": "configuredBy", + "type": "address", + "indexed": true, + "internalType": "address" + } + ], + "anonymous": false + }, { "type": "event", "name": "FundsWithdrawn", @@ -1957,6 +2145,61 @@ "name": "ContractPaused", "inputs": [] }, + { + "type": "error", + "name": "CreateYieldVaultConfigAlreadyExists", + "inputs": [ + { + "name": "configId", + "type": "uint64", + "internalType": "uint64" + } + ] + }, + { + "type": "error", + "name": "CreateYieldVaultConfigDisabled", + "inputs": [ + { + "name": "configId", + "type": "uint64", + "internalType": "uint64" + } + ] + }, + { + "type": "error", + "name": "CreateYieldVaultConfigNotFound", + "inputs": [ + { + "name": "configId", + "type": "uint64", + "internalType": "uint64" + } + ] + }, + { + "type": "error", + "name": "CreateYieldVaultConfigPairAlreadyRegistered", + "inputs": [ + { + "name": "configId", + "type": "uint64", + "internalType": "uint64" + } + ] + }, + { + "type": "error", + "name": "CreateYieldVaultConfigPairNotRegistered", + "inputs": [ + { + "name": "pairHash", + "type": "bytes32", + "internalType": "bytes32" + } + ] + }, { "type": "error", "name": "EmptyAddressArray", @@ -1998,6 +2241,11 @@ "name": "InvalidCOAAddress", "inputs": [] }, + { + "type": "error", + "name": "InvalidCreateYieldVaultConfigId", + "inputs": [] + }, { "type": "error", "name": "InvalidRequestState", diff --git a/local/deploy_and_verify.sh b/local/deploy_and_verify.sh index 2c24a2f..11bcb3a 100755 --- a/local/deploy_and_verify.sh +++ b/local/deploy_and_verify.sh @@ -16,6 +16,22 @@ set -a source "$PROJECT_ROOT/.env" set +a +# Fresh deployments should seed at least one CREATE_YIELDVAULT config so the system is usable immediately. +# Set SKIP_INITIAL_CREATE_CONFIG_REGISTRATION=1 only if you intentionally want to register configs later. +INITIAL_CREATE_VAULT_CONFIG_ID="${INITIAL_CREATE_VAULT_CONFIG_ID:-}" +INITIAL_CREATE_VAULT_IDENTIFIER="${INITIAL_CREATE_VAULT_IDENTIFIER:-}" +INITIAL_CREATE_STRATEGY_IDENTIFIER="${INITIAL_CREATE_STRATEGY_IDENTIFIER:-}" +SKIP_INITIAL_CREATE_CONFIG_REGISTRATION="${SKIP_INITIAL_CREATE_CONFIG_REGISTRATION:-0}" + +if [ "$SKIP_INITIAL_CREATE_CONFIG_REGISTRATION" != "1" ]; then + if [ -z "$INITIAL_CREATE_VAULT_CONFIG_ID" ] || [ -z "$INITIAL_CREATE_VAULT_IDENTIFIER" ] || [ -z "$INITIAL_CREATE_STRATEGY_IDENTIFIER" ]; then + echo "❌ Error: Missing initial CREATE_YIELDVAULT config." + echo "Set INITIAL_CREATE_VAULT_CONFIG_ID, INITIAL_CREATE_VAULT_IDENTIFIER, and INITIAL_CREATE_STRATEGY_IDENTIFIER in .env" + echo "or set SKIP_INITIAL_CREATE_CONFIG_REGISTRATION=1 if you intend to register configs manually later." + exit 1 + fi +fi + # Resolve testnet account address used by Cadence scripts. # Prefer .env override, then fallback to flow.json account config. TESTNET_ACCOUNT_ADDRESS="${TESTNET_ACCOUNT_ADDRESS:-}" @@ -78,6 +94,65 @@ is_incompatible_update_error() { [[ "$output" == *"cannot deploy invalid contract"* || "$output" == *"mismatching field"* || "$output" == *"found new field"* || "$output" == *"missing event declaration"* ]] } +register_initial_create_yieldvault_config() { + local contract_address="$1" + local registered_id + + if [ "$SKIP_INITIAL_CREATE_CONFIG_REGISTRATION" = "1" ]; then + echo "⚠️ Skipping initial CREATE_YIELDVAULT config registration because SKIP_INITIAL_CREATE_CONFIG_REGISTRATION=1" + echo "" + return 0 + fi + + echo "🧩 Registering initial CREATE_YIELDVAULT config..." + echo " Config ID: $INITIAL_CREATE_VAULT_CONFIG_ID" + echo " Vault: $INITIAL_CREATE_VAULT_IDENTIFIER" + echo " Strategy: $INITIAL_CREATE_STRATEGY_IDENTIFIER" + + registered_id=$(cast call "$contract_address" \ + "getCreateYieldVaultConfigId(string,string)(uint64)" \ + "$INITIAL_CREATE_VAULT_IDENTIFIER" \ + "$INITIAL_CREATE_STRATEGY_IDENTIFIER" \ + --rpc-url "$TESTNET_RPC_URL" | tr -d '[:space:]') + + if [ "$registered_id" = "$INITIAL_CREATE_VAULT_CONFIG_ID" ]; then + echo "✅ Initial CREATE_YIELDVAULT config already registered" + echo "" + return 0 + fi + + if [ -n "$registered_id" ] && [ "$registered_id" != "0" ]; then + echo "❌ CREATE_YIELDVAULT pair already registered with unexpected config ID" + echo "Expected config ID: $INITIAL_CREATE_VAULT_CONFIG_ID" + echo "Actual config ID: $registered_id" + exit 1 + fi + + flow_cmd transactions send "$PROJECT_ROOT/cadence/transactions/register_create_yieldvault_config_everywhere.cdc" \ + "$INITIAL_CREATE_VAULT_CONFIG_ID" \ + "$INITIAL_CREATE_VAULT_IDENTIFIER" \ + "$INITIAL_CREATE_STRATEGY_IDENTIFIER" \ + --network "$FLOW_NETWORK" \ + --signer "$FLOW_SIGNER" \ + --compute-limit 9999 + + registered_id=$(cast call "$contract_address" \ + "getCreateYieldVaultConfigId(string,string)(uint64)" \ + "$INITIAL_CREATE_VAULT_IDENTIFIER" \ + "$INITIAL_CREATE_STRATEGY_IDENTIFIER" \ + --rpc-url "$TESTNET_RPC_URL" | tr -d '[:space:]') + + if [ "$registered_id" != "$INITIAL_CREATE_VAULT_CONFIG_ID" ]; then + echo "❌ Failed to verify initial CREATE_YIELDVAULT config registration" + echo "Expected config ID: $INITIAL_CREATE_VAULT_CONFIG_ID" + echo "Actual config ID: ${registered_id:-}" + exit 1 + fi + + echo "✅ Initial CREATE_YIELDVAULT config registered" + echo "" +} + deploy_or_update_cadence_contract() { local contract_name="$1" local contract_path="$2" @@ -366,6 +441,11 @@ fi echo "" +# ========================================== +# Step 6.5: Seed Initial CREATE_YIELDVAULT Config +# ========================================== +register_initial_create_yieldvault_config "$DEPLOYED_ADDRESS" + # ========================================== # Step 7: Initialize WorkerOps Handlers & Schedule # ========================================== diff --git a/local/deploy_full_stack.sh b/local/deploy_full_stack.sh index 50d11bc..8d14604 100755 --- a/local/deploy_full_stack.sh +++ b/local/deploy_full_stack.sh @@ -24,8 +24,65 @@ USER_C_FUNDING="500.0" USER_D_EOA="0xE57bFE9F44b819898F47BF37E5AF72a0783e1141" USER_D_FUNDING="500.0" +# Fresh deployments need at least one registered CREATE_YIELDVAULT config. +# These defaults match the local emulator fixtures used by the E2E scripts. +INITIAL_CREATE_VAULT_CONFIG_ID="${INITIAL_CREATE_VAULT_CONFIG_ID:-1}" +INITIAL_CREATE_VAULT_IDENTIFIER="${INITIAL_CREATE_VAULT_IDENTIFIER:-A.0ae53cb6e3f42a79.FlowToken.Vault}" +INITIAL_CREATE_STRATEGY_IDENTIFIER="${INITIAL_CREATE_STRATEGY_IDENTIFIER:-A.045a1763c93006ca.MockStrategies.TracerStrategy}" + RPC_URL="localhost:8545" +register_initial_create_yieldvault_config() { + local contract_address=$1 + local registered_id + + echo "Registering initial CREATE_YIELDVAULT config..." + echo " Config ID: $INITIAL_CREATE_VAULT_CONFIG_ID" + echo " Vault: $INITIAL_CREATE_VAULT_IDENTIFIER" + echo " Strategy: $INITIAL_CREATE_STRATEGY_IDENTIFIER" + + registered_id=$(cast call "$contract_address" \ + "getCreateYieldVaultConfigId(string,string)(uint64)" \ + "$INITIAL_CREATE_VAULT_IDENTIFIER" \ + "$INITIAL_CREATE_STRATEGY_IDENTIFIER" \ + --rpc-url "http://$RPC_URL" | tr -d '[:space:]') + + if [ "$registered_id" = "$INITIAL_CREATE_VAULT_CONFIG_ID" ]; then + echo "✓ Initial CREATE_YIELDVAULT config already registered" + echo "" + return 0 + fi + + if [ -n "$registered_id" ] && [ "$registered_id" != "0" ]; then + echo "❌ CREATE_YIELDVAULT pair already registered with unexpected config ID" + echo "Expected config ID: $INITIAL_CREATE_VAULT_CONFIG_ID" + echo "Actual config ID: $registered_id" + exit 1 + fi + + flow transactions send ./cadence/transactions/register_create_yieldvault_config_everywhere.cdc \ + "$INITIAL_CREATE_VAULT_CONFIG_ID" \ + "$INITIAL_CREATE_VAULT_IDENTIFIER" \ + "$INITIAL_CREATE_STRATEGY_IDENTIFIER" \ + --signer emulator-flow-yield-vaults --compute-limit 9999 + + registered_id=$(cast call "$contract_address" \ + "getCreateYieldVaultConfigId(string,string)(uint64)" \ + "$INITIAL_CREATE_VAULT_IDENTIFIER" \ + "$INITIAL_CREATE_STRATEGY_IDENTIFIER" \ + --rpc-url "http://$RPC_URL" | tr -d '[:space:]') + + if [ "$registered_id" != "$INITIAL_CREATE_VAULT_CONFIG_ID" ]; then + echo "❌ Failed to verify initial CREATE_YIELDVAULT config registration" + echo "Expected config ID: $INITIAL_CREATE_VAULT_CONFIG_ID" + echo "Actual config ID: ${registered_id:-}" + exit 1 + fi + + echo "✓ Initial CREATE_YIELDVAULT config registered" + echo "" +} + # ============================================ # VERIFY EVM GATEWAY IS READY # ============================================ @@ -212,6 +269,8 @@ flow transactions send ./cadence/transactions/setup_worker_with_badge.cdc \ "$FLOW_VAULTS_REQUESTS_CONTRACT" \ --signer emulator-flow-yield-vaults --compute-limit 9999 +register_initial_create_yieldvault_config "$FLOW_VAULTS_REQUESTS_CONTRACT" + echo "✓ Project initialization complete" # Save contract address to file for other scripts diff --git a/local/run_admin_e2e_tests.sh b/local/run_admin_e2e_tests.sh index 99da795..a3ec692 100755 --- a/local/run_admin_e2e_tests.sh +++ b/local/run_admin_e2e_tests.sh @@ -172,12 +172,12 @@ get_escrow_balance() { } # Get request status (0=PENDING, 1=PROCESSING, 2=COMPLETED, 3=FAILED) -# The struct is: (requestId, user, requestType, status, token, amount, yieldVaultId, createdAt, vaultIdentifier, strategyIdentifier, errorMessage) +# The struct is: (requestId, user, requestType, status, token, amount, yieldVaultId, createdAt, errorMessage, createVaultConfigId, vaultIdentifier, strategyIdentifier) # Status is the 4th field (index 3) get_request_status() { local request_id=$1 local result - result=$(cast_call "getRequest(uint256)((uint256,address,uint8,uint8,address,uint256,uint64,uint256,string,string,string))" "$request_id") + result=$(cast_call "getRequest(uint256)((uint256,address,uint8,uint8,address,uint256,uint64,uint256,string,uint64,string,string))" "$request_id") # Extract the status field (4th value after the opening paren) # Cast output format: (requestId, user, requestType, status, ...) # Use awk to split by comma and get the 4th field, then extract the number diff --git a/local/run_e2e_tests.sh b/local/run_e2e_tests.sh index 6aabeb7..401a9f7 100755 --- a/local/run_e2e_tests.sh +++ b/local/run_e2e_tests.sh @@ -138,14 +138,14 @@ get_pending_count() { # Get request status (0=PENDING, 1=PROCESSING, 2=COMPLETED, 3=FAILED) get_request_status() { local request_id=$1 - cast_call "getRequest(uint256)((uint256,address,uint8,uint8,address,uint256,uint64,uint256,string,string,string))" "$request_id" | \ + cast_call "getRequest(uint256)((uint256,address,uint8,uint8,address,uint256,uint64,uint256,string,uint64,string,string))" "$request_id" | \ sed -n 's/.*(\([0-9]*\), [^,]*, [0-9]*, \([0-9]*\),.*/\2/p' } # Get YieldVault ID from completed request get_yieldvault_id_from_request() { local request_id=$1 - cast_call "getRequest(uint256)((uint256,address,uint8,uint8,address,uint256,uint64,uint256,string,string,string))" "$request_id" | \ + cast_call "getRequest(uint256)((uint256,address,uint8,uint8,address,uint256,uint64,uint256,string,uint64,string,string))" "$request_id" | \ grep -Eo '[0-9]+' | sed -n '7p' # 7th number is the yieldVaultId } diff --git a/local/run_worker_tests.sh b/local/run_worker_tests.sh index 076b190..ff6d90f 100755 --- a/local/run_worker_tests.sh +++ b/local/run_worker_tests.sh @@ -145,7 +145,7 @@ get_request_status() { local request_id=$1 # Use getRequestUnpacked which returns fields separately - status is the 4th return value (index 3) local result=$(cast call "$FLOW_VAULTS_REQUESTS_CONTRACT" \ - "getRequestUnpacked(uint256)(uint256,address,uint8,uint8,address,uint256,uint64,uint256,string,string,string)" \ + "getRequestUnpacked(uint256)(uint256,address,uint8,uint8,address,uint256,uint64,uint256,string,uint64)" \ "$request_id" \ --rpc-url "$RPC_URL" 2>/dev/null) # The output has each field on a separate line, status (uint8) is the 4th line @@ -202,7 +202,7 @@ get_claimable_refund() { get_request_message() { local request_id=$1 # Get the full request and extract the message field (9th field in the tuple) - local result=$(cast_call "getRequest(uint256)((uint256,address,uint8,uint8,address,uint256,uint64,uint256,string,string,string))" "$request_id" 2>&1) + local result=$(cast_call "getRequest(uint256)((uint256,address,uint8,uint8,address,uint256,uint64,uint256,string,uint64,string,string))" "$request_id" 2>&1) # Extract the first quoted string which is the message field echo "$result" | grep -oE '"[^"]*"' | head -1 | tr -d '"' } @@ -758,23 +758,26 @@ else fi # ============================================ -# SCENARIO 4: PANIC RECOVERY - INVALID STRATEGY -# ============================================ +# SCENARIO 4: ADMISSION REJECTION - UNREGISTERED STRATEGY +# ======================================================= -log_section "SCENARIO 4: Panic Recovery - Invalid Strategy Identifier" +log_section "SCENARIO 4: Admission Rejection - Unregistered Strategy Pair" -# This test verifies that requests with invalid strategy identifiers -# are caught during preprocessing and marked as FAILED with proper error messages +# Under the config-registry model, invalid CREATE_YIELDVAULT pairs should never +# reach the scheduler. The request must be rejected on EVM admission. # Record initial state USER_A_REFUND_BEFORE=$(get_claimable_refund "$USER_A_EOA") USER_A_REFUND_BEFORE=$(clean_wei "$USER_A_REFUND_BEFORE") -log_test "Create YieldVault request with invalid strategy identifier" +log_test "Create YieldVault request with unregistered strategy identifier" -# Use an invalid strategy identifier (not a valid Cadence type) +# Use an unregistered strategy identifier INVALID_STRATEGY="InvalidStrategy.NotReal" +PENDING_BEFORE_INVALID=$(get_pending_count) +PENDING_BEFORE_INVALID=$(clean_wei "$PENDING_BEFORE_INVALID") +set +e TX_OUTPUT=$(cast_send "$USER_A_PK" \ "createYieldVault(address,uint256,string,string)" \ "$NATIVE_FLOW" \ @@ -782,110 +785,38 @@ TX_OUTPUT=$(cast_send "$USER_A_PK" \ "$VAULT_IDENTIFIER" \ "$INVALID_STRATEGY" \ --value "1ether" 2>&1) +TX_STATUS=$? +set -e -INVALID_REQUEST_ID="" - -if echo "$TX_OUTPUT" | grep -q "status.*1"; then - log_success "Invalid strategy request submitted" - - # Extract request ID from the logs in TX_OUTPUT - # The RequestCreated event has requestId as the second topic (topics[1]) - # Event signature: RequestCreated(uint256 indexed requestId, address indexed user, ...) - # Look for the RequestCreated event log (has 4 topics) and get topics[1] - # The pattern 0x000...000X where X is a small hex number is the requestId - INVALID_REQUEST_ID=$(echo "$TX_OUTPUT" | grep -oE '"0x0{60,62}[0-9a-fA-F]{1,4}"' | head -1 | tr -d '"' || true) - - if [ -n "$INVALID_REQUEST_ID" ]; then - # Convert hex to decimal - INVALID_REQUEST_ID=$(printf "%d" "$INVALID_REQUEST_ID" 2>/dev/null || echo "") - fi - - log_info "New request ID: $INVALID_REQUEST_ID" - - if [ -z "$INVALID_REQUEST_ID" ]; then - log_fail "Could not determine request ID from transaction logs" - fi +if [ "$TX_STATUS" -ne 0 ]; then + log_success "Unregistered strategy pair rejected at admission" else - log_fail "Failed to submit invalid strategy request" + log_fail "Unregistered strategy pair should have been rejected" echo "$TX_OUTPUT" fi -log_test "Wait for request to be marked as FAILED" - -if [ -z "$INVALID_REQUEST_ID" ]; then - log_fail "Cannot check status - no request ID available" -else - # Wait for the scheduler to preprocess and fail the request - # Status 3 = FAILED - REQUEST_STATUS="" - WAIT_COUNTER=0 - MAX_WAIT=$((AUTO_PROCESS_TIMEOUT + 5)) - - while [ $WAIT_COUNTER -lt $MAX_WAIT ]; do - tick_emulator - - REQUEST_STATUS=$(get_request_status "$INVALID_REQUEST_ID") - # Status 3 = FAILED, Status 2 = COMPLETED - if [ "$REQUEST_STATUS" = "3" ]; then - log_info "Request $INVALID_REQUEST_ID reached FAILED status after ${WAIT_COUNTER}s" - break - elif [ "$REQUEST_STATUS" = "2" ]; then - log_warn "Request unexpectedly completed successfully" - break - fi - - sleep 1 - WAIT_COUNTER=$((WAIT_COUNTER + 1)) - - if [ $((WAIT_COUNTER % 5)) -eq 0 ]; then - log_info "Still waiting... (${WAIT_COUNTER}s, status: $REQUEST_STATUS)" - fi - done - - if [ "$REQUEST_STATUS" = "3" ]; then - log_success "Request correctly marked as FAILED (status: 3)" +log_test "Verify no request was enqueued" +PENDING_AFTER_INVALID=$(get_pending_count) +PENDING_AFTER_INVALID=$(clean_wei "$PENDING_AFTER_INVALID") +assert_eq "$PENDING_BEFORE_INVALID" "$PENDING_AFTER_INVALID" "Pending request count unchanged after admission rejection" - # Optionally check the error message - ERROR_MSG=$(get_request_message "$INVALID_REQUEST_ID") - if [ -n "$ERROR_MSG" ]; then - log_info "Error message: $ERROR_MSG" - fi - else - log_fail "Request not marked as FAILED (status: $REQUEST_STATUS)" - fi -fi +log_test "Verify no refund was created" -log_test "Verify refund was credited for failed request" - -# Check that the user's claimable refund increased USER_A_REFUND_AFTER=$(get_claimable_refund "$USER_A_EOA") USER_A_REFUND_AFTER=$(clean_wei "$USER_A_REFUND_AFTER") log_info "User A claimable refund: $USER_A_REFUND_BEFORE -> $USER_A_REFUND_AFTER wei" -# Expected refund is 1 ether = 1000000000000000000 wei -EXPECTED_REFUND_INCREASE="1000000000000000000" - -if compare_wei "$USER_A_REFUND_AFTER" -gt "$USER_A_REFUND_BEFORE"; then - REFUND_INCREASE=$(subtract_wei "$USER_A_REFUND_AFTER" "$USER_A_REFUND_BEFORE") - if compare_wei "$REFUND_INCREASE" -ge "$EXPECTED_REFUND_INCREASE"; then - log_success "Refund credited correctly ($(wei_to_ether $REFUND_INCREASE) FLOW)" - else - log_warn "Refund credited but amount differs: expected $EXPECTED_REFUND_INCREASE, got $REFUND_INCREASE" - log_success "Refund was credited" - fi -else - log_fail "No refund credited for failed request" -fi +assert_eq "$USER_A_REFUND_BEFORE" "$USER_A_REFUND_AFTER" "Claimable refund unchanged after admission rejection" # ============================================ -# SCENARIO 5: PREPROCESSING VALIDATION TESTS +# SCENARIO 5: ADMISSION REJECTION TESTS # ============================================ -log_section "SCENARIO 5: Preprocessing Validation Tests" +log_section "SCENARIO 5: Admission Rejection Tests" -# This test verifies that the preprocessing logic correctly rejects -# various types of invalid requests +# This test verifies that invalid or unsupported CREATE_YIELDVAULT pairs +# are rejected before a request is created. # --- Test Case A: Invalid vaultIdentifier --- @@ -894,9 +825,12 @@ log_test "Test Case A: Create request with invalid vaultIdentifier" USER_B_REFUND_BEFORE=$(get_claimable_refund "$USER_B_EOA") USER_B_REFUND_BEFORE=$(clean_wei "$USER_B_REFUND_BEFORE") -# Use an invalid vault identifier (not a valid Cadence type) +# Use an invalid vault identifier INVALID_VAULT="InvalidVault.NotReal" +PENDING_BEFORE_PREPROCESS=$(get_pending_count) +PENDING_BEFORE_PREPROCESS=$(clean_wei "$PENDING_BEFORE_PREPROCESS") +set +e TX_OUTPUT_A=$(cast_send "$USER_B_PK" \ "createYieldVault(address,uint256,string,string)" \ "$NATIVE_FLOW" \ @@ -904,11 +838,13 @@ TX_OUTPUT_A=$(cast_send "$USER_B_PK" \ "$INVALID_VAULT" \ "$STRATEGY_IDENTIFIER" \ --value "1ether" 2>&1) +TX_STATUS_A=$? +set -e -if echo "$TX_OUTPUT_A" | grep -q "status.*1"; then - log_success "Invalid vault identifier request submitted" +if [ "$TX_STATUS_A" -ne 0 ]; then + log_success "Invalid vault identifier pair rejected at admission" else - log_fail "Failed to submit invalid vault identifier request" + log_fail "Invalid vault identifier pair should have been rejected" echo "$TX_OUTPUT_A" fi @@ -919,10 +855,10 @@ log_test "Test Case B: Create request with unsupported strategy type" USER_C_REFUND_BEFORE=$(get_claimable_refund "$USER_C_EOA") USER_C_REFUND_BEFORE=$(clean_wei "$USER_C_REFUND_BEFORE") -# Use a valid Cadence type that is not a supported strategy -# FlowToken.Vault is a valid type but not a strategy +# Use an unregistered strategy type UNSUPPORTED_STRATEGY="A.${CADENCE_CONTRACT_ADDR}.FlowToken.Vault" +set +e TX_OUTPUT_B=$(cast_send "$USER_C_PK" \ "createYieldVault(address,uint256,string,string)" \ "$NATIVE_FLOW" \ @@ -930,90 +866,40 @@ TX_OUTPUT_B=$(cast_send "$USER_C_PK" \ "$VAULT_IDENTIFIER" \ "$UNSUPPORTED_STRATEGY" \ --value "1ether" 2>&1) +TX_STATUS_B=$? +set -e -if echo "$TX_OUTPUT_B" | grep -q "status.*1"; then - log_success "Unsupported strategy request submitted" +if [ "$TX_STATUS_B" -ne 0 ]; then + log_success "Unsupported strategy pair rejected at admission" else - log_fail "Failed to submit unsupported strategy request" + log_fail "Unsupported strategy pair should have been rejected" echo "$TX_OUTPUT_B" fi -log_test "Wait for preprocessing to fail both invalid requests" - -# Get pending count before waiting -PENDING_BEFORE_PREPROCESS=$(get_pending_count) -PENDING_BEFORE_PREPROCESS=$(clean_wei "$PENDING_BEFORE_PREPROCESS") - -# Wait for scheduler to preprocess and fail both requests -log_info "Waiting for scheduler to process invalid requests (pending: $PENDING_BEFORE_PREPROCESS)..." -sleep $((SCHEDULER_WAKEUP_INTERVAL * 2)) - -# Trigger emulator processing multiple times -for i in $(seq 1 12); do - tick_emulator - sleep 1 - if [ $((i % 4)) -eq 0 ]; then - CURRENT_PENDING=$(get_pending_count) - CURRENT_PENDING=$(clean_wei "$CURRENT_PENDING") - log_info "Processing... (${i}s elapsed, pending: $CURRENT_PENDING)" - fi -done - -# Verify both requests were processed (removed from pending) +log_test "Verify no invalid requests were enqueued" PENDING_AFTER_PREPROCESS=$(get_pending_count) PENDING_AFTER_PREPROCESS=$(clean_wei "$PENDING_AFTER_PREPROCESS") -REQUESTS_PROCESSED=$((PENDING_BEFORE_PREPROCESS - PENDING_AFTER_PREPROCESS)) log_info "Pending: $PENDING_BEFORE_PREPROCESS -> $PENDING_AFTER_PREPROCESS" +assert_eq "$PENDING_BEFORE_PREPROCESS" "$PENDING_AFTER_PREPROCESS" "Pending request count unchanged for invalid pairs" -if [ "$PENDING_AFTER_PREPROCESS" -eq 0 ]; then - log_success "Both invalid requests were processed by scheduler" -else - log_fail "Expected all requests to be processed (pending: $PENDING_AFTER_PREPROCESS)" -fi - -log_test "Verify refund was credited for invalid vault identifier request" +log_test "Verify no refund was created for invalid vault identifier" USER_B_REFUND_AFTER=$(get_claimable_refund "$USER_B_EOA") USER_B_REFUND_AFTER=$(clean_wei "$USER_B_REFUND_AFTER") log_info "User B claimable refund: $USER_B_REFUND_BEFORE -> $USER_B_REFUND_AFTER wei" -# Expected refund is 1 ether -EXPECTED_REFUND="1000000000000000000" +assert_eq "$USER_B_REFUND_BEFORE" "$USER_B_REFUND_AFTER" "Claimable refund unchanged for invalid vault identifier" -if compare_wei "$USER_B_REFUND_AFTER" -gt "$USER_B_REFUND_BEFORE"; then - REFUND_INCREASE=$(subtract_wei "$USER_B_REFUND_AFTER" "$USER_B_REFUND_BEFORE") - log_info "User B refund increase: $(wei_to_ether $REFUND_INCREASE) FLOW" - if compare_wei "$REFUND_INCREASE" -ge "$EXPECTED_REFUND"; then - log_success "Invalid vaultIdentifier request failed and refund credited" - else - log_warn "Refund credited but amount differs from expected" - log_success "Refund was credited" - fi -else - log_fail "No refund credited for invalid vaultIdentifier request" -fi - -log_test "Verify refund was credited for unsupported strategy request" +log_test "Verify no refund was created for unsupported strategy" USER_C_REFUND_AFTER=$(get_claimable_refund "$USER_C_EOA") USER_C_REFUND_AFTER=$(clean_wei "$USER_C_REFUND_AFTER") log_info "User C claimable refund: $USER_C_REFUND_BEFORE -> $USER_C_REFUND_AFTER wei" -if compare_wei "$USER_C_REFUND_AFTER" -gt "$USER_C_REFUND_BEFORE"; then - REFUND_INCREASE=$(subtract_wei "$USER_C_REFUND_AFTER" "$USER_C_REFUND_BEFORE") - log_info "User C refund increase: $(wei_to_ether $REFUND_INCREASE) FLOW" - if compare_wei "$REFUND_INCREASE" -ge "$EXPECTED_REFUND"; then - log_success "Unsupported strategy request failed and refund credited" - else - log_warn "Refund credited but amount differs from expected" - log_success "Refund was credited" - fi -else - log_fail "No refund credited for unsupported strategy request" -fi +assert_eq "$USER_C_REFUND_BEFORE" "$USER_C_REFUND_AFTER" "Claimable refund unchanged for unsupported strategy" # ============================================ # SCENARIO 6: MAX PROCESSING CAPACITY TEST diff --git a/local/testnet-e2e.sh b/local/testnet-e2e.sh index ec144a3..20558e3 100755 --- a/local/testnet-e2e.sh +++ b/local/testnet-e2e.sh @@ -72,9 +72,9 @@ # - Contract balance: 0 (funds bridged to Cadence) # - YieldVault: +amount (minus small bridging fee ~0.0002 FLOW) # -# SCENARIO 2: INVALID PARAMETERS (Error Handling) -# ------------------------------------------------ -# When vaultIdentifier or strategyIdentifier are invalid: +# SCENARIO 2: UNREGISTERED PARAMETERS (Admission Rejection) +# --------------------------------------------------------- +# When vaultIdentifier or strategyIdentifier do not match a registered pair: # # # Invalid vault, correct strategy # ./local/testnet-e2e.sh create-flow 1.5 "InvalidVault" "A.d2580caf2ef07c2f.FlowYieldVaultsStrategies.mUSDCStrategy" @@ -83,30 +83,21 @@ # ./local/testnet-e2e.sh create-flow 1.7 "A.7e60df042a9c0868.FlowToken.Vault" "InvalidStrategy" # # Expected behavior: -# 1. Request created with status PENDING (EVM contract doesn't validate identifiers) -# 2. SchedulerHandler picks up request -# 4. Preprocessing: preprocessRequests() attempts to parse identifiers on Cadence side -# 5. Validation fails: "Invalid vaultIdentifier/strategyIdentifier: X is not a valid Cadence type" -# 6. PENDING -> FAILED -# 7. No YieldVault created, yieldVaultId set to NO_YIELDVAULT_ID (max uint64) +# 1. EVM admission rejects the transaction immediately +# 2. No request is created, no escrow is taken, and no scheduler/worker processing happens +# 3. User only pays the reverted transaction gas cost # # Balance changes: -# - User wallet: -amount (+ gas fees) - funds left wallet -# - Pending balance: 0 (escrow was deducted at startProcessingBatch) -# - Contract balance: +amount (funds returned by COA during completeProcessing) -# - COA balance: unchanged (funds returned to contract) +# - User wallet: gas only +# - Pending balance: unchanged +# - Contract balance: unchanged +# - COA balance: unchanged # - YieldVault: none created # # REFUND MECHANISM: # ----------------- -# When a CREATE/DEPOSIT request fails/panics after PROCESSING state: -# 1. PROCESSING state transfers funds: Contract -> COA -# 2. SchedulerHandler detects validation failure in case of panic -# 3. completeProcessing(FAILED) is called with refund: -# - Native FLOW: COA sends funds back via msg.value -# - ERC20 (WFLOW): COA approves contract, then contract pulls via transferFrom -# 4. Contract receives funds and credits claimableRefunds -# 5. User can claim the refund via claimRefund() +# Refunds only exist for requests that were accepted, escrowed, and later failed during processing. +# Unregistered CREATE_YIELDVAULT pairs are rejected before escrow, so there is no refund to claim. # # ============================================================================= # TYPICAL TEST FLOW @@ -151,9 +142,10 @@ DEFAULT_CADENCE_CONTRACT="0x764bdff06a0ee77e" REFUND_CHECK_MAX_ATTEMPTS="${REFUND_CHECK_MAX_ATTEMPTS:-60}" REFUND_CHECK_DELAY_SECONDS="${REFUND_CHECK_DELAY_SECONDS:-5}" -# Default correct parameters -DEFAULT_VAULT="A.7e60df042a9c0868.FlowToken.Vault" -DEFAULT_STRATEGY="A.d2580caf2ef07c2f.FlowYieldVaultsStrategies.mUSDCStrategy" +# Default registered parameters +DEFAULT_CREATE_CONFIG_ID="${INITIAL_CREATE_VAULT_CONFIG_ID:-1}" +DEFAULT_VAULT="${INITIAL_CREATE_VAULT_IDENTIFIER:-A.7e60df042a9c0868.FlowToken.Vault}" +DEFAULT_STRATEGY="${INITIAL_CREATE_STRATEGY_IDENTIFIER:-A.d2580caf2ef07c2f.FlowYieldVaultsStrategies.mUSDCStrategy}" DEFAULT_PYUSD0_VAULT="A.dfc20aee650fcbdf.EVMVMBridgedToken_d7d43ab7b365f0d0789ae83f4385fa710ffdc98f.Vault" DEFAULT_PYUSD0_STRATEGY="A.d2580caf2ef07c2f.PMStrategiesV1.FUSDEVStrategy" PYUSD0_DECIMALS=6 @@ -241,7 +233,7 @@ extract_tx_hash() { get_request_status() { local request_id=$1 - local result=$(cast call "$CONTRACT" "getRequest(uint256)((uint256,address,uint8,uint8,address,uint256,uint64,uint256,string,string,string))" "$request_id" --rpc-url "$RPC_URL") + local result=$(cast call "$CONTRACT" "getRequest(uint256)((uint256,address,uint8,uint8,address,uint256,uint64,uint256,string,uint64,string,string))" "$request_id" --rpc-url "$RPC_URL") echo "$result" | sed 's/[()]//g' | cut -d',' -f4 | tr -d ' ' } @@ -661,6 +653,11 @@ refund_check() { local amount=$1 local vault="${2:-InvalidVault}" local strategy="${3:-InvalidStrategy}" + local amount_wei + local refund_before + local refund_after + local tx_out + local tx_status if [ -z "$PRIVATE_KEY" ]; then print_error "PRIVATE_KEY is required" @@ -678,15 +675,20 @@ refund_check() { fi validate_amount "$amount" - local amount_wei=$(ether_to_wei "$amount") + amount_wei=$(ether_to_wei "$amount") - print_header "Refund Check (forced failure)" + refund_before=$(cast call "$CONTRACT" "getClaimableRefund(address,address)(uint256)" \ + "$USER" \ + "$NATIVE_FLOW" \ + --rpc-url "$RPC_URL" | tr -d '[:space:]') + + print_header "Admission Rejection Check" echo "Amount: $amount FLOW" echo "Vault: $vault" echo "Strategy: $strategy" echo "" - local tx_out + set +e tx_out=$(cast send "$CONTRACT" "createYieldVault(address,uint256,string,string)" \ "$NATIVE_FLOW" \ "$amount_wei" \ @@ -694,30 +696,32 @@ refund_check() { "$strategy" \ --value "$amount_wei" \ --private-key "$PRIVATE_KEY" \ - --rpc-url "$RPC_URL") + --rpc-url "$RPC_URL" 2>&1) + tx_status=$? + set -e - local tx_hash - tx_hash=$(extract_tx_hash "$tx_out") - if [ -z "$tx_hash" ]; then - print_error "Could not parse transaction hash from send output." + if [ "$tx_status" -eq 0 ]; then + print_error "Transaction unexpectedly succeeded. The pair appears to be registered." echo "$tx_out" exit 1 fi - print_success "Transaction sent: $tx_hash" - local request_id - request_id=$(extract_request_id "$tx_hash") - if [ -z "$request_id" ]; then - print_error "Could not extract requestId from receipt." + print_success "Unregistered pair rejected before escrow" + echo "$tx_out" + + refund_after=$(cast call "$CONTRACT" "getClaimableRefund(address,address)(uint256)" \ + "$USER" \ + "$NATIVE_FLOW" \ + --rpc-url "$RPC_URL" | tr -d '[:space:]') + + if [ "$refund_before" != "$refund_after" ]; then + print_error "Claimable refund changed unexpectedly after admission rejection" + echo "Before: $refund_before" + echo "After: $refund_after" exit 1 fi - print_success "Request ID: $request_id" - wait_for_request_status "$request_id" 3 "$REFUND_CHECK_MAX_ATTEMPTS" "$REFUND_CHECK_DELAY_SECONDS" - - get_user_claimable_refund "$USER" "$NATIVE_FLOW" - claim_refund "$NATIVE_FLOW" - check_evm_state + print_success "No refund was created, as expected" } # ============================================================================= @@ -728,12 +732,12 @@ get_request() { local request_id=$1 print_header "Request $request_id Details" - local result=$(cast call "$CONTRACT" "getRequest(uint256)((uint256,address,uint8,uint8,address,uint256,uint64,uint256,string,string,string))" "$request_id" --rpc-url "$RPC_URL") + local result=$(cast call "$CONTRACT" "getRequest(uint256)((uint256,address,uint8,uint8,address,uint256,uint64,uint256,string,uint64,string,string))" "$request_id" --rpc-url "$RPC_URL") echo "$result" # Parse status (4th field in the tuple: id, user, requestType, status, ...) - # Format: (id, address, type, status, token, amount, vaultId, timestamp, msg, vault, strategy) + # Format: (id, address, type, status, token, amount, vaultId, timestamp, msg, configId, vault, strategy) local status=$(echo "$result" | sed 's/[()]//g' | cut -d',' -f4 | tr -d ' ') case $status in 0) echo -e "\nStatus: ${YELLOW}PENDING${NC}" ;; @@ -1016,7 +1020,7 @@ show_help() { echo " Create YieldVault with PYUSD0 (6 decimals)" echo " Uses PMStrategiesV1.FUSDEVStrategy by default" echo " refund-check [vault] [strategy]" - echo " Force failure, then claim refund (defaults: InvalidVault/InvalidStrategy)" + echo " Verify an unregistered pair is rejected before escrow" echo " claim-refund [token]" echo " Claim refund for token (default: NATIVE_FLOW)" echo " request Get request details" @@ -1049,6 +1053,7 @@ show_help() { echo "DEFAULT PARAMETERS:" echo " Vault: $DEFAULT_VAULT" echo " Strategy: $DEFAULT_STRATEGY" + echo " Config ID: $DEFAULT_CREATE_CONFIG_ID" echo " NATIVE_FLOW: $NATIVE_FLOW" echo " WFLOW: $WFLOW" echo " PYUSD0: $PYUSD0" @@ -1059,10 +1064,10 @@ show_help() { echo " $0 state" echo " $0 create-flow 1.2" echo " $0 create-flow 1.2 x100 # Create 100 requests" - echo " $0 create-flow 1.5 InvalidVault InvalidStrategy" + echo " $0 create-flow 1.5 $DEFAULT_VAULT $DEFAULT_STRATEGY" echo " $0 create-wflow 1.0 x50 # Create 50 WFLOW requests" echo " $0 create-pyusd0 1.0 # Create 1 PYUSD0-backed request" - echo " $0 refund-check 0.1" + echo " $0 refund-check 0.1 InvalidVault InvalidStrategy" echo " $0 request 10" echo "" echo " # Admin queries" diff --git a/solidity/script/FlowYieldVaultsYieldVaultOperations.s.sol b/solidity/script/FlowYieldVaultsYieldVaultOperations.s.sol index 8e7eca3..4ded516 100644 --- a/solidity/script/FlowYieldVaultsYieldVaultOperations.s.sol +++ b/solidity/script/FlowYieldVaultsYieldVaultOperations.s.sol @@ -34,8 +34,7 @@ import {FlowYieldVaultsRequests} from "../src/FlowYieldVaultsRequests.sol"; * Environment Variables: * - USER_PRIVATE_KEY: Private key for signing transactions (defaults to 0x3 for testing) * - AMOUNT: Amount in wei for create/deposit operations (defaults to 10 ether) - * - VAULT_IDENTIFIER: Cadence vault type identifier (defaults to emulator address) - * - STRATEGY_IDENTIFIER: Cadence strategy type identifier (defaults to emulator address) + * - CREATE_VAULT_CONFIG_ID: Registered CREATE_YIELDVAULT config ID (defaults to 1) */ contract FlowYieldVaultsYieldVaultOperations is Script { /// @dev Sentinel address for native $FLOW (must match FlowYieldVaultsRequests.NATIVE_FLOW) @@ -44,26 +43,19 @@ contract FlowYieldVaultsYieldVaultOperations is Script { /// @dev Default amount for create/deposit operations uint256 constant DEFAULT_AMOUNT = 10 ether; - /// @dev Default vault identifier (emulator) - string constant DEFAULT_VAULT_IDENTIFIER = - "A.0ae53cb6e3f42a79.FlowToken.Vault"; - - /// @dev Default strategy identifier (emulator) - string constant DEFAULT_STRATEGY_IDENTIFIER = - "A.045a1763c93006ca.MockStrategies.TracerStrategy"; + /// @dev Default CREATE_YIELDVAULT config ID registered by local deploy scripts + uint64 constant DEFAULT_CREATE_VAULT_CONFIG_ID = 1; /// @notice Creates a new YieldVault by depositing native $FLOW /// @param contractAddress The FlowYieldVaultsRequests contract address function createYieldVault(address contractAddress) public { (uint256 privateKey, address user) = _getUser(); uint256 amount = vm.envOr("AMOUNT", DEFAULT_AMOUNT); - string memory vaultId = vm.envOr( - "VAULT_IDENTIFIER", - DEFAULT_VAULT_IDENTIFIER - ); - string memory strategyId = vm.envOr( - "STRATEGY_IDENTIFIER", - DEFAULT_STRATEGY_IDENTIFIER + uint64 createVaultConfigId = uint64( + vm.envOr( + "CREATE_VAULT_CONFIG_ID", + uint256(DEFAULT_CREATE_VAULT_CONFIG_ID) + ) ); FlowYieldVaultsRequests requests = FlowYieldVaultsRequests( @@ -76,12 +68,12 @@ contract FlowYieldVaultsYieldVaultOperations is Script { uint256 requestId = requests.createYieldVault{value: amount}( NATIVE_FLOW, amount, - vaultId, - strategyId + createVaultConfigId ); vm.stopBroadcast(); _logRequestCreated("CREATE_YIELDVAULT", requestId, user, amount); + console.log("Create config ID:", uint256(createVaultConfigId)); _logRequestDetails(requests, requestId); } @@ -180,7 +172,7 @@ contract FlowYieldVaultsYieldVaultOperations is Script { FlowYieldVaultsRequests requests = FlowYieldVaultsRequests( payable(contractAddress) ); - FlowYieldVaultsRequests.Request memory req = requests.getRequest( + FlowYieldVaultsRequests.RequestView memory req = requests.getRequest( requestId ); @@ -274,7 +266,7 @@ contract FlowYieldVaultsYieldVaultOperations is Script { FlowYieldVaultsRequests requests, uint256 requestId ) internal view { - FlowYieldVaultsRequests.Request memory req = requests.getRequest( + FlowYieldVaultsRequests.RequestView memory req = requests.getRequest( requestId ); diff --git a/solidity/src/FlowYieldVaultsRequests.sol b/solidity/src/FlowYieldVaultsRequests.sol index 7b4bef1..b80b421 100644 --- a/solidity/src/FlowYieldVaultsRequests.sol +++ b/solidity/src/FlowYieldVaultsRequests.sol @@ -54,7 +54,7 @@ contract FlowYieldVaultsRequests is ReentrancyGuard, Ownable2Step { FAILED } - /// @notice Complete request data structure + /// @notice Stored request data structure /// @param id Unique request identifier /// @param user Address of the user who created the request /// @param requestType Type of operation requested @@ -64,8 +64,7 @@ contract FlowYieldVaultsRequests is ReentrancyGuard, Ownable2Step { /// @param yieldVaultId Associated YieldVault Id /// @param timestamp Block timestamp when request was created /// @param message Status message or error reason - /// @param vaultIdentifier Cadence vault type identifier for CREATE_YIELDVAULT - /// @param strategyIdentifier Cadence strategy type identifier for CREATE_YIELDVAULT + /// @param createVaultConfigId Immutable CREATE_YIELDVAULT config ID (0 for non-CREATE requests) struct Request { uint256 id; address user; @@ -76,6 +75,29 @@ contract FlowYieldVaultsRequests is ReentrancyGuard, Ownable2Step { uint64 yieldVaultId; uint256 timestamp; string message; + uint64 createVaultConfigId; + } + + /// @notice Fully resolved request view including the immutable CREATE_YIELDVAULT config ID + struct RequestView { + uint256 id; + address user; + RequestType requestType; + RequestStatus status; + address tokenAddress; + uint256 amount; + uint64 yieldVaultId; + uint256 timestamp; + string message; + uint64 createVaultConfigId; + string vaultIdentifier; + string strategyIdentifier; + } + + /// @notice Immutable CREATE_YIELDVAULT configuration selected by config ID + struct CreateYieldVaultConfig { + bool exists; + bool enabled; string vaultIdentifier; string strategyIdentifier; } @@ -167,6 +189,12 @@ contract FlowYieldVaultsRequests is ReentrancyGuard, Ownable2Step { /// @notice All requests indexed by request ID mapping(uint256 => Request) public requests; + /// @notice Immutable CREATE_YIELDVAULT configs indexed by config ID + mapping(uint64 => CreateYieldVaultConfig) private _createYieldVaultConfigs; + + /// @notice Reverse lookup from (vaultIdentifier,strategyIdentifier) pair to config ID + mapping(bytes32 => uint64) public createYieldVaultConfigIdByPairHash; + /// @notice Array of pending request IDs awaiting processing (FIFO order) uint256[] public pendingRequestIds; @@ -270,6 +298,24 @@ contract FlowYieldVaultsRequests is ReentrancyGuard, Ownable2Step { /// @notice Strategy identifier cannot be empty for CREATE_YIELDVAULT error EmptyStrategyIdentifier(); + /// @notice CREATE_YIELDVAULT config ID cannot be zero + error InvalidCreateYieldVaultConfigId(); + + /// @notice CREATE_YIELDVAULT config does not exist + error CreateYieldVaultConfigNotFound(uint64 configId); + + /// @notice CREATE_YIELDVAULT config already exists + error CreateYieldVaultConfigAlreadyExists(uint64 configId); + + /// @notice CREATE_YIELDVAULT config cannot be used for new requests + error CreateYieldVaultConfigDisabled(uint64 configId); + + /// @notice The provided vault/strategy pair is already mapped to a config ID + error CreateYieldVaultConfigPairAlreadyRegistered(uint64 configId); + + /// @notice The provided vault/strategy pair is not registered + error CreateYieldVaultConfigPairNotRegistered(bytes32 pairHash); + /// @notice No refund available for the specified token error NoRefundAvailable(address token); @@ -299,6 +345,28 @@ contract FlowYieldVaultsRequests is ReentrancyGuard, Ownable2Step { string strategyIdentifier ); + /// @notice Emitted when a CREATE_YIELDVAULT config is registered + /// @param configId Immutable config ID + /// @param vaultIdentifier Cadence vault type identifier + /// @param strategyIdentifier Cadence strategy type identifier + /// @param configuredBy Admin who registered the config + event CreateYieldVaultConfigRegistered( + uint64 indexed configId, + string vaultIdentifier, + string strategyIdentifier, + address indexed configuredBy + ); + + /// @notice Emitted when a CREATE_YIELDVAULT config is enabled or disabled for new requests + /// @param configId Immutable config ID + /// @param enabled Whether new requests may use this config + /// @param updatedBy Admin who changed the status + event CreateYieldVaultConfigEnabled( + uint64 indexed configId, + bool enabled, + address indexed updatedBy + ); + /// @notice Emitted when a request status changes /// @param requestId Request being updated /// @param user Address of the user who owns the request @@ -642,6 +710,67 @@ contract FlowYieldVaultsRequests is ReentrancyGuard, Ownable2Step { ); } + /// @notice Registers an immutable CREATE_YIELDVAULT config + /// @dev Config IDs are immutable once registered. Disable old configs instead of mutating them. + /// @param configId Immutable config ID + /// @param vaultIdentifier Cadence vault type identifier + /// @param strategyIdentifier Cadence strategy type identifier + function registerCreateYieldVaultConfig( + uint64 configId, + string calldata vaultIdentifier, + string calldata strategyIdentifier + ) external onlyOwner { + if (configId == 0) revert InvalidCreateYieldVaultConfigId(); + if (_createYieldVaultConfigs[configId].exists) { + revert CreateYieldVaultConfigAlreadyExists(configId); + } + if (bytes(vaultIdentifier).length == 0) revert EmptyVaultIdentifier(); + if (bytes(strategyIdentifier).length == 0) { + revert EmptyStrategyIdentifier(); + } + + bytes32 pairHash = _hashCreateYieldVaultConfigPair( + vaultIdentifier, + strategyIdentifier + ); + uint64 existingConfigId = createYieldVaultConfigIdByPairHash[pairHash]; + if (existingConfigId != 0) { + revert CreateYieldVaultConfigPairAlreadyRegistered(existingConfigId); + } + + _createYieldVaultConfigs[configId] = CreateYieldVaultConfig({ + exists: true, + enabled: true, + vaultIdentifier: vaultIdentifier, + strategyIdentifier: strategyIdentifier + }); + createYieldVaultConfigIdByPairHash[pairHash] = configId; + + emit CreateYieldVaultConfigRegistered( + configId, + vaultIdentifier, + strategyIdentifier, + msg.sender + ); + emit CreateYieldVaultConfigEnabled(configId, true, msg.sender); + } + + /// @notice Enables or disables a CREATE_YIELDVAULT config for new requests + /// @param configId Immutable config ID + /// @param enabled Whether new requests may use this config + function setCreateYieldVaultConfigEnabled( + uint64 configId, + bool enabled + ) external onlyOwner { + CreateYieldVaultConfig storage config = _createYieldVaultConfigs[ + configId + ]; + if (!config.exists) revert CreateYieldVaultConfigNotFound(configId); + + config.enabled = enabled; + emit CreateYieldVaultConfigEnabled(configId, enabled, msg.sender); + } + /// @notice Sets the maximum pending requests allowed per user /// @param _maxRequests New limit (0 = unlimited) function setMaxPendingRequestsPerUser( @@ -683,7 +812,29 @@ contract FlowYieldVaultsRequests is ReentrancyGuard, Ownable2Step { // External Functions - User // ============================================ - /// @notice Creates a new YieldVault by depositing funds + /// @notice Creates a new YieldVault by depositing funds with a registered config ID + /// @param tokenAddress Token to deposit (use NATIVE_FLOW for native $FLOW) + /// @param amount Amount to deposit + /// @param createVaultConfigId Immutable CREATE_YIELDVAULT config ID + /// @return requestId The created request ID + function createYieldVault( + address tokenAddress, + uint256 amount, + uint64 createVaultConfigId + ) + external + payable + whenNotPaused + onlyAllowlisted + notBlocklisted + nonReentrant + returns (uint256) + { + return _createYieldVaultRequest(tokenAddress, amount, createVaultConfigId); + } + + /// @notice Creates a new YieldVault by depositing funds using a registered vault/strategy pair + /// @dev Legacy compatibility path. Resolves the pair to a config ID before creating the request. /// @param tokenAddress Token to deposit (use NATIVE_FLOW for native $FLOW) /// @param amount Amount to deposit /// @param vaultIdentifier Cadence vault type identifier @@ -708,17 +859,22 @@ contract FlowYieldVaultsRequests is ReentrancyGuard, Ownable2Step { if (bytes(strategyIdentifier).length == 0) revert EmptyStrategyIdentifier(); - _validateDeposit(tokenAddress, amount); - _checkPendingRequestLimit(msg.sender); + bytes32 pairHash = _hashCreateYieldVaultConfigPair( + vaultIdentifier, + strategyIdentifier + ); + uint64 createVaultConfigId = createYieldVaultConfigIdByPairHash[ + pairHash + ]; + if (createVaultConfigId == 0) { + revert CreateYieldVaultConfigPairNotRegistered(pairHash); + } return - _createRequest( - RequestType.CREATE_YIELDVAULT, + _createYieldVaultRequest( tokenAddress, amount, - NO_YIELDVAULT_ID, - vaultIdentifier, - strategyIdentifier + createVaultConfigId ); } @@ -756,8 +912,7 @@ contract FlowYieldVaultsRequests is ReentrancyGuard, Ownable2Step { vaultToken, amount, yieldVaultId, - "", - "" + 0 ); } @@ -781,8 +936,7 @@ contract FlowYieldVaultsRequests is ReentrancyGuard, Ownable2Step { vaultToken, amount, yieldVaultId, - "", - "" + 0 ); } @@ -803,8 +957,7 @@ contract FlowYieldVaultsRequests is ReentrancyGuard, Ownable2Step { vaultToken, 0, yieldVaultId, - "", - "" + 0 ); } @@ -1081,8 +1234,54 @@ contract FlowYieldVaultsRequests is ReentrancyGuard, Ownable2Step { return pendingRequestIds; } + /// @notice Gets a CREATE_YIELDVAULT config by ID + /// @param configId Immutable config ID + /// @return exists Whether the config exists + /// @return enabled Whether new requests may use this config + /// @return vaultIdentifier Cadence vault type identifier + /// @return strategyIdentifier Cadence strategy type identifier + function getCreateYieldVaultConfig( + uint64 configId + ) + external + view + returns ( + bool exists, + bool enabled, + string memory vaultIdentifier, + string memory strategyIdentifier + ) + { + CreateYieldVaultConfig storage config = _createYieldVaultConfigs[ + configId + ]; + return ( + config.exists, + config.enabled, + config.vaultIdentifier, + config.strategyIdentifier + ); + } + + /// @notice Resolves a vault/strategy pair to a registered config ID + /// @param vaultIdentifier Cadence vault type identifier + /// @param strategyIdentifier Cadence strategy type identifier + /// @return configId Registered config ID or 0 if not found + function getCreateYieldVaultConfigId( + string calldata vaultIdentifier, + string calldata strategyIdentifier + ) external view returns (uint64 configId) { + return + createYieldVaultConfigIdByPairHash[ + _hashCreateYieldVaultConfigPair( + vaultIdentifier, + strategyIdentifier + ) + ]; + } + /// @notice Gets pending requests in unpacked format with pagination - /// @dev Optimized for Cadence consumption - returns parallel arrays instead of struct array + /// @dev Legacy read API that resolves CREATE_YIELDVAULT config IDs back to identifier strings /// @param startIndex Starting index in pending requests /// @param count Number of requests to return (0 = all remaining) /// @return ids Request IDs @@ -1094,8 +1293,7 @@ contract FlowYieldVaultsRequests is ReentrancyGuard, Ownable2Step { /// @return yieldVaultIds YieldVault Ids /// @return timestamps Timestamps /// @return messages Messages - /// @return vaultIdentifiers Vault identifiers - /// @return strategyIdentifiers Strategy identifiers + /// @return createVaultConfigIds Immutable CREATE_YIELDVAULT config IDs function getPendingRequestsUnpacked( uint256 startIndex, uint256 count @@ -1112,8 +1310,7 @@ contract FlowYieldVaultsRequests is ReentrancyGuard, Ownable2Step { uint64[] memory yieldVaultIds, uint256[] memory timestamps, string[] memory messages, - string[] memory vaultIdentifiers, - string[] memory strategyIdentifiers + uint64[] memory createVaultConfigIds ) { if (startIndex >= pendingRequestIds.length) { @@ -1127,8 +1324,7 @@ contract FlowYieldVaultsRequests is ReentrancyGuard, Ownable2Step { new uint64[](0), new uint256[](0), new string[](0), - new string[](0), - new string[](0) + new uint64[](0) ); } @@ -1146,11 +1342,10 @@ contract FlowYieldVaultsRequests is ReentrancyGuard, Ownable2Step { yieldVaultIds = new uint64[](size); timestamps = new uint256[](size); messages = new string[](size); - vaultIdentifiers = new string[](size); - strategyIdentifiers = new string[](size); + createVaultConfigIds = new uint64[](size); for (uint256 i = 0; i < size; ) { - Request memory req = requests[pendingRequestIds[startIndex + i]]; + Request storage req = requests[pendingRequestIds[startIndex + i]]; ids[i] = req.id; users[i] = req.user; requestTypes[i] = uint8(req.requestType); @@ -1160,8 +1355,7 @@ contract FlowYieldVaultsRequests is ReentrancyGuard, Ownable2Step { yieldVaultIds[i] = req.yieldVaultId; timestamps[i] = req.timestamp; messages[i] = req.message; - vaultIdentifiers[i] = req.vaultIdentifier; - strategyIdentifiers[i] = req.strategyIdentifier; + createVaultConfigIds[i] = req.createVaultConfigId; unchecked { ++i; } @@ -1173,8 +1367,28 @@ contract FlowYieldVaultsRequests is ReentrancyGuard, Ownable2Step { /// @return Request data function getRequest( uint256 requestId - ) external view returns (Request memory) { - return requests[requestId]; + ) external view returns (RequestView memory) { + Request storage req = requests[requestId]; + ( + string memory vaultIdentifier, + string memory strategyIdentifier + ) = _resolveCreateYieldVaultConfigForRequest(req); + + return + RequestView({ + id: req.id, + user: req.user, + requestType: req.requestType, + status: req.status, + tokenAddress: req.tokenAddress, + amount: req.amount, + yieldVaultId: req.yieldVaultId, + timestamp: req.timestamp, + message: req.message, + createVaultConfigId: req.createVaultConfigId, + vaultIdentifier: vaultIdentifier, + strategyIdentifier: strategyIdentifier + }); } /// @notice Gets a specific request by ID in unpacked format (tuple) @@ -1188,8 +1402,7 @@ contract FlowYieldVaultsRequests is ReentrancyGuard, Ownable2Step { /// @return yieldVaultId YieldVault Id /// @return timestamp Timestamp /// @return message Status message - /// @return vaultIdentifier Vault identifier - /// @return strategyIdentifier Strategy identifier + /// @return createVaultConfigId Immutable CREATE_YIELDVAULT config ID function getRequestUnpacked( uint256 requestId ) @@ -1205,9 +1418,8 @@ contract FlowYieldVaultsRequests is ReentrancyGuard, Ownable2Step { uint64 yieldVaultId, uint256 timestamp, string memory message, - string memory vaultIdentifier, - string memory strategyIdentifier - ) + uint64 createVaultConfigId + ) { Request storage req = requests[requestId]; id = req.id; @@ -1219,8 +1431,7 @@ contract FlowYieldVaultsRequests is ReentrancyGuard, Ownable2Step { yieldVaultId = req.yieldVaultId; timestamp = req.timestamp; message = req.message; - vaultIdentifier = req.vaultIdentifier; - strategyIdentifier = req.strategyIdentifier; + createVaultConfigId = req.createVaultConfigId; } /// @notice Checks if a YieldVault Id is valid @@ -1272,8 +1483,7 @@ contract FlowYieldVaultsRequests is ReentrancyGuard, Ownable2Step { /// @return yieldVaultIds YieldVault Ids /// @return timestamps Timestamps /// @return messages Messages - /// @return vaultIdentifiers Vault identifiers - /// @return strategyIdentifiers Strategy identifiers + /// @return createVaultConfigIds Immutable CREATE_YIELDVAULT config IDs /// @return pendingBalance Escrowed balance for active pending requests (native FLOW only) /// @return claimableRefund Claimable refund amount (native FLOW only) function getPendingRequestsByUserUnpacked( @@ -1290,8 +1500,7 @@ contract FlowYieldVaultsRequests is ReentrancyGuard, Ownable2Step { uint64[] memory yieldVaultIds, uint256[] memory timestamps, string[] memory messages, - string[] memory vaultIdentifiers, - string[] memory strategyIdentifiers, + uint64[] memory createVaultConfigIds, uint256 pendingBalance, uint256 claimableRefund ) @@ -1309,8 +1518,7 @@ contract FlowYieldVaultsRequests is ReentrancyGuard, Ownable2Step { yieldVaultIds = new uint64[](count); timestamps = new uint256[](count); messages = new string[](count); - vaultIdentifiers = new string[](count); - strategyIdentifiers = new string[](count); + createVaultConfigIds = new uint64[](count); // Fill arrays - only iterate through user's requests for (uint256 i = 0; i < count; ) { @@ -1323,8 +1531,7 @@ contract FlowYieldVaultsRequests is ReentrancyGuard, Ownable2Step { yieldVaultIds[i] = req.yieldVaultId; timestamps[i] = req.timestamp; messages[i] = req.message; - vaultIdentifiers[i] = req.vaultIdentifier; - strategyIdentifiers[i] = req.strategyIdentifier; + createVaultConfigIds[i] = req.createVaultConfigId; unchecked { ++i; } @@ -1700,6 +1907,43 @@ contract FlowYieldVaultsRequests is ReentrancyGuard, Ownable2Step { emit YieldVaultIdUnregistered(yieldVaultId, user, requestId); } + /** + * @dev Validates a registered CREATE_YIELDVAULT config and creates the request. + * @param tokenAddress The token involved in the CREATE_YIELDVAULT request. + * @param amount The amount of tokens involved. + * @param createVaultConfigId Immutable CREATE_YIELDVAULT config ID. + * @return The newly created request ID. + */ + function _createYieldVaultRequest( + address tokenAddress, + uint256 amount, + uint64 createVaultConfigId + ) internal returns (uint256) { + if (createVaultConfigId == 0) revert InvalidCreateYieldVaultConfigId(); + + CreateYieldVaultConfig storage config = _createYieldVaultConfigs[ + createVaultConfigId + ]; + if (!config.exists) { + revert CreateYieldVaultConfigNotFound(createVaultConfigId); + } + if (!config.enabled) { + revert CreateYieldVaultConfigDisabled(createVaultConfigId); + } + + _validateDeposit(tokenAddress, amount); + _checkPendingRequestLimit(msg.sender); + + return + _createRequest( + RequestType.CREATE_YIELDVAULT, + tokenAddress, + amount, + NO_YIELDVAULT_ID, + createVaultConfigId + ); + } + /** * @dev Creates a new request and updates all related state. * This function handles the core request creation logic shared by all request types: @@ -1712,8 +1956,7 @@ contract FlowYieldVaultsRequests is ReentrancyGuard, Ownable2Step { * @param tokenAddress The token involved in this request. * @param amount The amount of tokens involved (0 for CLOSE requests). * @param yieldVaultId The YieldVault Id - * @param vaultIdentifier Cadence vault type identifier (only for CREATE requests). - * @param strategyIdentifier Cadence strategy type identifier (only for CREATE requests). + * @param createVaultConfigId Immutable CREATE_YIELDVAULT config ID (0 for non-CREATE requests). * @return The newly created request ID. */ function _createRequest( @@ -1721,8 +1964,7 @@ contract FlowYieldVaultsRequests is ReentrancyGuard, Ownable2Step { address tokenAddress, uint256 amount, uint64 yieldVaultId, - string memory vaultIdentifier, - string memory strategyIdentifier + uint64 createVaultConfigId ) internal returns (uint256) { // Generate unique request ID using auto-incrementing counter uint256 requestId = _requestIdCounter++; @@ -1738,8 +1980,7 @@ contract FlowYieldVaultsRequests is ReentrancyGuard, Ownable2Step { yieldVaultId: yieldVaultId, timestamp: block.timestamp, message: "", - vaultIdentifier: vaultIdentifier, - strategyIdentifier: strategyIdentifier + createVaultConfigId: createVaultConfigId }); // Add to global pending queue with index tracking for O(1) lookup @@ -1764,6 +2005,11 @@ contract FlowYieldVaultsRequests is ReentrancyGuard, Ownable2Step { ); } + ( + string memory vaultIdentifier, + string memory strategyIdentifier + ) = _resolveCreateYieldVaultConfigForRequest(requests[requestId]); + emit RequestCreated( requestId, msg.sender, @@ -1779,6 +2025,53 @@ contract FlowYieldVaultsRequests is ReentrancyGuard, Ownable2Step { return requestId; } + /** + * @dev Resolves a stored request's CREATE_YIELDVAULT config into identifier strings. + * Non-CREATE requests, or CREATE requests whose config is missing, resolve to empty strings. + * @param req The stored request. + * @return vaultIdentifier Cadence vault type identifier or empty string. + * @return strategyIdentifier Cadence strategy type identifier or empty string. + */ + function _resolveCreateYieldVaultConfigForRequest( + Request storage req + ) + internal + view + returns ( + string memory vaultIdentifier, + string memory strategyIdentifier + ) + { + if ( + req.requestType != RequestType.CREATE_YIELDVAULT || + req.createVaultConfigId == 0 + ) { + return ("", ""); + } + + CreateYieldVaultConfig storage config = _createYieldVaultConfigs[ + req.createVaultConfigId + ]; + if (!config.exists) { + return ("", ""); + } + + return (config.vaultIdentifier, config.strategyIdentifier); + } + + /** + * @dev Hashes a CREATE_YIELDVAULT config pair for reverse lookup. + * @param vaultIdentifier Cadence vault type identifier. + * @param strategyIdentifier Cadence strategy type identifier. + * @return Pair hash. + */ + function _hashCreateYieldVaultConfigPair( + string memory vaultIdentifier, + string memory strategyIdentifier + ) internal pure returns (bytes32) { + return keccak256(abi.encode(vaultIdentifier, strategyIdentifier)); + } + /** * @dev Removes a request from all pending queues while preserving request history. * Uses two different removal strategies: diff --git a/solidity/test/FlowYieldVaultsRequests.t.sol b/solidity/test/FlowYieldVaultsRequests.t.sol index a7fff0a..58de4fd 100644 --- a/solidity/test/FlowYieldVaultsRequests.t.sol +++ b/solidity/test/FlowYieldVaultsRequests.t.sol @@ -38,6 +38,7 @@ contract FlowYieldVaultsRequestsTest is Test { error OwnableUnauthorizedAccount(address account); error OwnableInvalidOwner(address owner); + uint64 constant CREATE_CONFIG_ID = 1; string constant VAULT_ID = "A.0ae53cb6e3f42a79.FlowToken.Vault"; string constant STRATEGY_ID = "A.045a1763c93006ca.MockStrategies.TracerStrategy"; @@ -45,6 +46,7 @@ contract FlowYieldVaultsRequestsTest is Test { vm.deal(user, 100 ether); vm.deal(user2, 100 ether); c = new FlowYieldVaultsRequestsTestHelper(coa, WFLOW); + c.registerCreateYieldVaultConfig(CREATE_CONFIG_ID, VAULT_ID, STRATEGY_ID); c.testRegisterYieldVaultId(42, user, NATIVE_FLOW); } @@ -66,10 +68,41 @@ contract FlowYieldVaultsRequestsTest is Test { assertEq(c.getUserPendingBalance(user, NATIVE_FLOW), 1 ether); assertEq(c.getPendingRequestCount(), 1); - FlowYieldVaultsRequests.Request memory req = c.getRequest(reqId); + FlowYieldVaultsRequests.RequestView memory req = c.getRequest(reqId); assertEq(uint8(req.requestType), uint8(FlowYieldVaultsRequests.RequestType.CREATE_YIELDVAULT)); assertEq(req.user, user); assertEq(req.amount, 1 ether); + assertEq(req.createVaultConfigId, CREATE_CONFIG_ID); + assertEq(req.vaultIdentifier, VAULT_ID); + assertEq(req.strategyIdentifier, STRATEGY_ID); + } + + function test_CreateYieldVault_WithConfigId() public { + vm.prank(user); + uint256 reqId = c.createYieldVault{value: 1 ether}( + NATIVE_FLOW, + 1 ether, + CREATE_CONFIG_ID + ); + + FlowYieldVaultsRequests.RequestView memory req = c.getRequest(reqId); + assertEq(req.createVaultConfigId, CREATE_CONFIG_ID); + assertEq(req.vaultIdentifier, VAULT_ID); + assertEq(req.strategyIdentifier, STRATEGY_ID); + } + + function test_GetCreateYieldVaultConfig() public view { + ( + bool exists, + bool enabled, + string memory vaultIdentifier, + string memory strategyIdentifier + ) = c.getCreateYieldVaultConfig(CREATE_CONFIG_ID); + + assertTrue(exists); + assertTrue(enabled); + assertEq(vaultIdentifier, VAULT_ID); + assertEq(strategyIdentifier, STRATEGY_ID); } function test_CreateYieldVault_UsesSentinelYieldVaultIdPlaceholder() public { @@ -77,7 +110,7 @@ contract FlowYieldVaultsRequestsTest is Test { 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); + FlowYieldVaultsRequests.RequestView memory req = c.getRequest(reqId); assertEq( req.yieldVaultId, sentinelYieldVaultId, @@ -115,7 +148,7 @@ contract FlowYieldVaultsRequestsTest is Test { vm.prank(user); uint256 reqId = c.depositToYieldVault{value: 1 ether}(42, NATIVE_FLOW, 1 ether); - FlowYieldVaultsRequests.Request memory req = c.getRequest(reqId); + FlowYieldVaultsRequests.RequestView memory req = c.getRequest(reqId); assertEq(uint8(req.requestType), uint8(FlowYieldVaultsRequests.RequestType.DEPOSIT_TO_YIELDVAULT)); assertEq(req.yieldVaultId, 42); assertEq(req.tokenAddress, NATIVE_FLOW); @@ -138,7 +171,7 @@ contract FlowYieldVaultsRequestsTest is Test { vm.prank(user); uint256 reqId = c.withdrawFromYieldVault(42, 0.5 ether); - FlowYieldVaultsRequests.Request memory req = c.getRequest(reqId); + FlowYieldVaultsRequests.RequestView memory req = c.getRequest(reqId); assertEq(uint8(req.requestType), uint8(FlowYieldVaultsRequests.RequestType.WITHDRAW_FROM_YIELDVAULT)); assertEq(req.amount, 0.5 ether); assertEq(req.tokenAddress, NATIVE_FLOW); @@ -150,7 +183,7 @@ contract FlowYieldVaultsRequestsTest is Test { vm.prank(user); uint256 reqId = c.withdrawFromYieldVault(43, 0.5 ether); - FlowYieldVaultsRequests.Request memory req = c.getRequest(reqId); + FlowYieldVaultsRequests.RequestView memory req = c.getRequest(reqId); assertEq(req.tokenAddress, WFLOW); } @@ -158,7 +191,7 @@ contract FlowYieldVaultsRequestsTest is Test { vm.prank(user); uint256 reqId = c.closeYieldVault(42); - FlowYieldVaultsRequests.Request memory req = c.getRequest(reqId); + FlowYieldVaultsRequests.RequestView memory req = c.getRequest(reqId); assertEq(uint8(req.requestType), uint8(FlowYieldVaultsRequests.RequestType.CLOSE_YIELDVAULT)); assertEq(req.yieldVaultId, 42); assertEq(req.tokenAddress, NATIVE_FLOW); @@ -170,7 +203,7 @@ contract FlowYieldVaultsRequestsTest is Test { vm.prank(user); uint256 reqId = c.closeYieldVault(44); - FlowYieldVaultsRequests.Request memory req = c.getRequest(reqId); + FlowYieldVaultsRequests.RequestView memory req = c.getRequest(reqId); assertEq(req.tokenAddress, WFLOW); } @@ -187,7 +220,7 @@ contract FlowYieldVaultsRequestsTest is Test { assertEq(c.getClaimableRefund(user, NATIVE_FLOW), 1 ether); // Funds claimable assertEq(c.getPendingRequestCount(), 0); - FlowYieldVaultsRequests.Request memory req = c.getRequest(reqId); + FlowYieldVaultsRequests.RequestView memory req = c.getRequest(reqId); assertEq(uint8(req.status), uint8(FlowYieldVaultsRequests.RequestStatus.FAILED)); // User claims refund @@ -314,7 +347,7 @@ contract FlowYieldVaultsRequestsTest is Test { // Balance deducted atomically assertEq(c.getUserPendingBalance(user, NATIVE_FLOW), 0); - FlowYieldVaultsRequests.Request memory req = c.getRequest(reqId); + FlowYieldVaultsRequests.RequestView memory req = c.getRequest(reqId); assertEq(uint8(req.status), uint8(FlowYieldVaultsRequests.RequestStatus.PROCESSING)); } @@ -348,7 +381,7 @@ contract FlowYieldVaultsRequestsTest is Test { c.completeProcessing(reqId, true, 100, "YieldVault created"); vm.stopPrank(); - FlowYieldVaultsRequests.Request memory req = c.getRequest(reqId); + FlowYieldVaultsRequests.RequestView memory req = c.getRequest(reqId); assertEq(uint8(req.status), uint8(FlowYieldVaultsRequests.RequestStatus.COMPLETED)); assertEq(req.yieldVaultId, 100); assertEq(c.getPendingRequestCount(), 0); @@ -376,7 +409,7 @@ contract FlowYieldVaultsRequestsTest is Test { assertEq(c.getUserPendingBalance(user, NATIVE_FLOW), 0); assertEq(c.getClaimableRefund(user, NATIVE_FLOW), 1 ether); - FlowYieldVaultsRequests.Request memory req = c.getRequest(reqId); + FlowYieldVaultsRequests.RequestView memory req = c.getRequest(reqId); assertEq(uint8(req.status), uint8(FlowYieldVaultsRequests.RequestStatus.FAILED)); } @@ -398,7 +431,7 @@ contract FlowYieldVaultsRequestsTest is Test { assertEq(c.getUserPendingBalance(user, NATIVE_FLOW), 0); assertEq(c.getClaimableRefund(user, NATIVE_FLOW), 1 ether); - FlowYieldVaultsRequests.Request memory req = c.getRequest(reqId); + FlowYieldVaultsRequests.RequestView memory req = c.getRequest(reqId); assertEq(uint8(req.requestType), uint8(FlowYieldVaultsRequests.RequestType.DEPOSIT_TO_YIELDVAULT)); assertEq(req.yieldVaultId, 42); assertEq(uint8(req.status), uint8(FlowYieldVaultsRequests.RequestStatus.FAILED)); @@ -494,7 +527,7 @@ contract FlowYieldVaultsRequestsTest is Test { assertEq(c.getClaimableRefund(user, NATIVE_FLOW), 1 ether); // Funds claimable assertEq(c.getPendingRequestCount(), 0); - FlowYieldVaultsRequests.Request memory req = c.getRequest(reqId); + FlowYieldVaultsRequests.RequestView memory req = c.getRequest(reqId); assertEq(uint8(req.status), uint8(FlowYieldVaultsRequests.RequestStatus.FAILED)); // User claims refund @@ -674,12 +707,60 @@ contract FlowYieldVaultsRequestsTest is Test { c.createYieldVault{value: 1 ether}(NATIVE_FLOW, 1 ether, "", ""); } + function test_CreateYieldVault_RevertUnknownConfigId() public { + vm.prank(user); + vm.expectRevert( + abi.encodeWithSelector( + FlowYieldVaultsRequests.CreateYieldVaultConfigNotFound.selector, + uint64(999) + ) + ); + c.createYieldVault{value: 1 ether}(NATIVE_FLOW, 1 ether, uint64(999)); + } + + function test_CreateYieldVault_RevertDisabledConfigId() public { + vm.prank(c.owner()); + c.setCreateYieldVaultConfigEnabled(CREATE_CONFIG_ID, false); + + vm.prank(user); + vm.expectRevert( + abi.encodeWithSelector( + FlowYieldVaultsRequests.CreateYieldVaultConfigDisabled.selector, + CREATE_CONFIG_ID + ) + ); + c.createYieldVault{value: 1 ether}(NATIVE_FLOW, 1 ether, CREATE_CONFIG_ID); + } + + function test_CreateYieldVault_RevertUnregisteredPair() public { + bytes32 pairHash = keccak256( + abi.encode( + "A.0ae53cb6e3f42a79.FlowToken.Vault", + "A.9999999999999999.Unknown.Strategy" + ) + ); + + vm.prank(user); + vm.expectRevert( + abi.encodeWithSelector( + FlowYieldVaultsRequests.CreateYieldVaultConfigPairNotRegistered.selector, + pairHash + ) + ); + c.createYieldVault{value: 1 ether}( + NATIVE_FLOW, + 1 ether, + "A.0ae53cb6e3f42a79.FlowToken.Vault", + "A.9999999999999999.Unknown.Strategy" + ); + } + function test_DepositToYieldVault_AnyoneCanDeposit() public { // YieldVault 42 is owned by user, but user2 can deposit to it vm.prank(user2); uint256 reqId = c.depositToYieldVault{value: 1 ether}(42, NATIVE_FLOW, 1 ether); - FlowYieldVaultsRequests.Request memory req = c.getRequest(reqId); + FlowYieldVaultsRequests.RequestView memory req = c.getRequest(reqId); assertEq(uint8(req.requestType), uint8(FlowYieldVaultsRequests.RequestType.DEPOSIT_TO_YIELDVAULT)); assertEq(req.yieldVaultId, 42); assertEq(req.user, user2); @@ -722,7 +803,6 @@ contract FlowYieldVaultsRequestsTest is Test { , , , - , ) = c.getPendingRequestsUnpacked(0, 0); @@ -736,6 +816,61 @@ contract FlowYieldVaultsRequestsTest is Test { assertEq(requestTypes[1], uint8(FlowYieldVaultsRequests.RequestType.DEPOSIT_TO_YIELDVAULT)); } + function test_GetPendingRequestsUnpacked_UsesConfigIds() public { + vm.startPrank(user); + c.createYieldVault{value: 1 ether}(NATIVE_FLOW, 1 ether, CREATE_CONFIG_ID); + c.depositToYieldVault{value: 2 ether}(42, NATIVE_FLOW, 2 ether); + vm.stopPrank(); + + ( + uint256[] memory ids, + , + uint8[] memory requestTypes, + , + , + , + , + , + , + uint64[] memory createVaultConfigIds + ) = c.getPendingRequestsUnpacked(0, 0); + + assertEq(ids.length, 2); + assertEq(requestTypes[0], uint8(FlowYieldVaultsRequests.RequestType.CREATE_YIELDVAULT)); + assertEq(requestTypes[1], uint8(FlowYieldVaultsRequests.RequestType.DEPOSIT_TO_YIELDVAULT)); + assertEq(createVaultConfigIds[0], CREATE_CONFIG_ID); + assertEq(createVaultConfigIds[1], 0); + } + + function test_GetRequestUnpacked_IncludesConfigId() public { + vm.prank(user); + uint256 reqId = c.createYieldVault{value: 1 ether}( + NATIVE_FLOW, + 1 ether, + CREATE_CONFIG_ID + ); + + ( + uint256 id, + address reqUser, + uint8 requestType, + , + , + uint256 amount, + uint64 yieldVaultId, + , + , + uint64 createVaultConfigId + ) = c.getRequestUnpacked(reqId); + + assertEq(id, reqId); + assertEq(reqUser, user); + assertEq(requestType, uint8(FlowYieldVaultsRequests.RequestType.CREATE_YIELDVAULT)); + assertEq(amount, 1 ether); + assertEq(yieldVaultId, c.NO_YIELDVAULT_ID()); + assertEq(createVaultConfigId, CREATE_CONFIG_ID); + } + function test_GetPendingRequestsUnpacked_Pagination() public { vm.startPrank(user); c.createYieldVault{value: 1 ether}(NATIVE_FLOW, 1 ether, VAULT_ID, STRATEGY_ID); @@ -744,13 +879,13 @@ contract FlowYieldVaultsRequestsTest is Test { vm.stopPrank(); // Get first 2 - (uint256[] memory ids, , , , , , , , , , ) = c.getPendingRequestsUnpacked(0, 2); + (uint256[] memory ids, , , , , , , , , ) = c.getPendingRequestsUnpacked(0, 2); assertEq(ids.length, 2); assertEq(ids[0], 1); assertEq(ids[1], 2); // Get starting from index 1 - (uint256[] memory ids2, , , , , , , , , , ) = c.getPendingRequestsUnpacked(1, 2); + (uint256[] memory ids2, , , , , , , , , ) = c.getPendingRequestsUnpacked(1, 2); assertEq(ids2.length, 2); assertEq(ids2[0], 2); assertEq(ids2[1], 3); @@ -779,7 +914,7 @@ contract FlowYieldVaultsRequestsTest is Test { assertEq(c.getPendingRequestCount(), 0); assertEq(c.doesUserOwnYieldVault(user, 100), true); - FlowYieldVaultsRequests.Request memory req = c.getRequest(reqId); + FlowYieldVaultsRequests.RequestView memory req = c.getRequest(reqId); assertEq(uint8(req.status), uint8(FlowYieldVaultsRequests.RequestStatus.COMPLETED)); assertEq(req.yieldVaultId, 100); } @@ -795,7 +930,7 @@ contract FlowYieldVaultsRequestsTest is Test { c.completeProcessing(reqId, true, 42, "Withdrawn"); vm.stopPrank(); - FlowYieldVaultsRequests.Request memory req = c.getRequest(reqId); + FlowYieldVaultsRequests.RequestView memory req = c.getRequest(reqId); assertEq(uint8(req.status), uint8(FlowYieldVaultsRequests.RequestStatus.COMPLETED)); } @@ -857,7 +992,7 @@ contract FlowYieldVaultsRequestsTest is Test { vm.stopPrank(); // Verify FIFO order is maintained: [req1, req2, req4, req5] - (uint256[] memory ids, , , , , , , , , , ) = c.getPendingRequestsUnpacked(0, 0); + (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"); @@ -878,7 +1013,7 @@ contract FlowYieldVaultsRequestsTest is Test { c.completeProcessing(req1, true, 100, "Created"); vm.stopPrank(); - (uint256[] memory ids, , , , , , , , , , ) = c.getPendingRequestsUnpacked(0, 0); + (uint256[] memory ids, , , , , , , , , ) = c.getPendingRequestsUnpacked(0, 0); assertEq(ids.length, 2); assertEq(ids[0], req2, "First should now be req2"); assertEq(ids[1], req3, "Second should be req3"); @@ -897,7 +1032,7 @@ contract FlowYieldVaultsRequestsTest is Test { c.completeProcessing(req3, true, 100, "Created"); vm.stopPrank(); - (uint256[] memory ids, , , , , , , , , , ) = c.getPendingRequestsUnpacked(0, 0); + (uint256[] memory ids, , , , , , , , , ) = c.getPendingRequestsUnpacked(0, 0); assertEq(ids.length, 2); assertEq(ids[0], req1); assertEq(ids[1], req2); @@ -939,7 +1074,7 @@ contract FlowYieldVaultsRequestsTest is Test { c.completeProcessing(req2, true, 100, "Created"); // After removing req2: [req1, req3, req4] - (uint256[] memory ids1, , , , , , , , , , ) = c.getPendingRequestsUnpacked(0, 0); + (uint256[] memory ids1, , , , , , , , , ) = c.getPendingRequestsUnpacked(0, 0); assertEq(ids1[0], req1); assertEq(ids1[1], req3); assertEq(ids1[2], req4); @@ -948,7 +1083,7 @@ contract FlowYieldVaultsRequestsTest is Test { c.completeProcessing(req4, true, 101, "Created"); // After removing req4: [req1, req3] - (uint256[] memory ids2, , , , , , , , , , ) = c.getPendingRequestsUnpacked(0, 0); + (uint256[] memory ids2, , , , , , , , , ) = c.getPendingRequestsUnpacked(0, 0); assertEq(ids2[0], req1); assertEq(ids2[1], req3); @@ -956,7 +1091,7 @@ contract FlowYieldVaultsRequestsTest is Test { c.completeProcessing(req1, true, 102, "Created"); // After removing req1: [req3] - (uint256[] memory ids3, , , , , , , , , , ) = c.getPendingRequestsUnpacked(0, 0); + (uint256[] memory ids3, , , , , , , , , ) = c.getPendingRequestsUnpacked(0, 0); assertEq(ids3.length, 1); assertEq(ids3[0], req3); @@ -986,9 +1121,9 @@ contract FlowYieldVaultsRequestsTest is Test { uint64[] memory yieldVaultIds, uint256[] memory timestamps, string[] memory messages, - string[] memory vaultIdentifiers, - string[] memory strategyIdentifiers, + uint64[] memory createVaultConfigIds, uint256 pendingBalance, + ) = c.getPendingRequestsByUserUnpacked(user); assertEq(ids.length, 2, "User should have 2 pending requests"); @@ -998,6 +1133,8 @@ contract FlowYieldVaultsRequestsTest is Test { assertEq(requestTypes[1], uint8(FlowYieldVaultsRequests.RequestType.DEPOSIT_TO_YIELDVAULT)); assertEq(amounts[0], 1 ether); assertEq(amounts[1], 2 ether); + assertEq(createVaultConfigIds[0], CREATE_CONFIG_ID); + assertEq(createVaultConfigIds[1], 0); assertEq(pendingBalance, 3 ether, "Pending balance should be sum of amounts"); } @@ -1012,12 +1149,12 @@ contract FlowYieldVaultsRequestsTest is Test { c.createYieldVault{value: 3 ether}(NATIVE_FLOW, 3 ether, VAULT_ID, STRATEGY_ID); // Check user's requests - (uint256[] memory userIds, , , , , , , , , , uint256 userBalance, ) = c.getPendingRequestsByUserUnpacked(user); + (uint256[] memory userIds, , , , , , , , , uint256 userBalance, ) = c.getPendingRequestsByUserUnpacked(user); assertEq(userIds.length, 2, "User should have 2 requests"); assertEq(userBalance, 4 ether, "User pending balance should be 4 ether"); // Check user2's requests - (uint256[] memory user2Ids, , , , , , , , , , uint256 user2Balance, ) = c.getPendingRequestsByUserUnpacked(user2); + (uint256[] memory user2Ids, , , , , , , , , uint256 user2Balance, ) = c.getPendingRequestsByUserUnpacked(user2); assertEq(user2Ids.length, 1, "User2 should have 1 request"); assertEq(user2Balance, 2 ether, "User2 pending balance should be 2 ether"); } @@ -1025,7 +1162,7 @@ contract FlowYieldVaultsRequestsTest is Test { function test_GetPendingRequestsByUserUnpacked_EmptyForNewUser() public { address newUser = makeAddr("newUser"); - (uint256[] memory ids, , , , , , , , , , uint256 pendingBalance, ) = c.getPendingRequestsByUserUnpacked(newUser); + (uint256[] memory ids, , , , , , , , , uint256 pendingBalance, ) = c.getPendingRequestsByUserUnpacked(newUser); assertEq(ids.length, 0, "New user should have no pending requests"); assertEq(pendingBalance, 0, "New user should have 0 pending balance"); @@ -1045,7 +1182,7 @@ contract FlowYieldVaultsRequestsTest is Test { vm.stopPrank(); // User should now have req1 and req3 - (uint256[] memory ids, , , , uint256[] memory amounts, , , , , , uint256 pendingBalance, ) = c.getPendingRequestsByUserUnpacked(user); + (uint256[] memory ids, , , , uint256[] memory amounts, , , , , uint256 pendingBalance, ) = 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"); @@ -1063,7 +1200,7 @@ contract FlowYieldVaultsRequestsTest is Test { c.cancelRequest(req1); vm.stopPrank(); - (uint256[] memory ids, , , , , , , , , , uint256 pendingBalance, uint256 claimableRefund) = c.getPendingRequestsByUserUnpacked(user); + (uint256[] memory ids, , , , , , , , , uint256 pendingBalance, uint256 claimableRefund) = c.getPendingRequestsByUserUnpacked(user); assertEq(ids.length, 1, "User should have 1 remaining request"); assertEq(ids[0], req2, "Remaining request should be req2"); // pendingBalance = 2 ether (escrowed for req2 only) @@ -1103,16 +1240,16 @@ contract FlowYieldVaultsRequestsTest is Test { vm.stopPrank(); // Verify user1's remaining requests - (uint256[] memory u1Ids, , , , , , , , , , , ) = c.getPendingRequestsByUserUnpacked(user); + (uint256[] memory u1Ids, , , , , , , , , , ) = c.getPendingRequestsByUserUnpacked(user); assertEq(u1Ids.length, 2, "User1 should have 2 requests"); // Verify user2's requests unchanged - (uint256[] memory u2Ids, , , , , , , , , , , ) = c.getPendingRequestsByUserUnpacked(user2); + (uint256[] memory u2Ids, , , , , , , , , , ) = c.getPendingRequestsByUserUnpacked(user2); assertEq(u2Ids.length, 1, "User2 should have 1 request"); assertEq(u2Ids[0], u2r1); // Verify user3's requests unchanged - (uint256[] memory u3Ids, , , , , , , , , , , ) = c.getPendingRequestsByUserUnpacked(user3); + (uint256[] memory u3Ids, , , , , , , , , , ) = c.getPendingRequestsByUserUnpacked(user3); assertEq(u3Ids.length, 1, "User3 should have 1 request"); assertEq(u3Ids[0], u3r1); } @@ -1130,7 +1267,7 @@ contract FlowYieldVaultsRequestsTest is Test { c.completeProcessing(req2, true, 101, "Created"); vm.stopPrank(); - (uint256[] memory ids, , , , , , , , , , uint256 pendingBalance, ) = c.getPendingRequestsByUserUnpacked(user); + (uint256[] memory ids, , , , , , , , , uint256 pendingBalance, ) = c.getPendingRequestsByUserUnpacked(user); assertEq(ids.length, 0, "User should have no pending requests"); assertEq(pendingBalance, 0); } @@ -1259,7 +1396,7 @@ contract FlowYieldVaultsRequestsTest is Test { assertEq(c.getPendingRequestCount(), numRequests / 2); // Verify FIFO order is maintained for remaining requests - (uint256[] memory ids, , , , , , , , , , ) = c.getPendingRequestsUnpacked(0, 0); + (uint256[] memory ids, , , , , , , , , ) = c.getPendingRequestsUnpacked(0, 0); assertEq(ids.length, numRequests / 2); // Even-indexed original requests should remain in order @@ -1294,7 +1431,7 @@ contract FlowYieldVaultsRequestsTest is Test { // Verify all other users still have 3 requests for (uint256 i = 0; i < 5; i++) { - (uint256[] memory ids, , , , , , , , , , , ) = c.getPendingRequestsByUserUnpacked(users[i]); + (uint256[] memory ids, , , , , , , , , , ) = c.getPendingRequestsByUserUnpacked(users[i]); if (i == 2) { assertEq(ids.length, 2, "User 2 should have 2 requests"); } else { @@ -1318,7 +1455,7 @@ contract FlowYieldVaultsRequestsTest is Test { assertEq(c.getPendingRequestCount(), 0); - (uint256[] memory ids, , , , , , , , , , , ) = c.getPendingRequestsByUserUnpacked(user); + (uint256[] memory ids, , , , , , , , , , ) = c.getPendingRequestsByUserUnpacked(user); assertEq(ids.length, 0); } @@ -1336,7 +1473,7 @@ contract FlowYieldVaultsRequestsTest is Test { vm.stopPrank(); // req1 should still be removed from pending (it's marked FAILED) - (uint256[] memory ids, , , , , , , , , , uint256 pendingBalance, uint256 claimableRefund) = c.getPendingRequestsByUserUnpacked(user); + (uint256[] memory ids, , , , , , , , , uint256 pendingBalance, uint256 claimableRefund) = c.getPendingRequestsByUserUnpacked(user); assertEq(ids.length, 1, "Should have 1 pending request"); assertEq(ids[0], req2); // Escrowed balance only includes req2 @@ -1356,13 +1493,13 @@ contract FlowYieldVaultsRequestsTest is Test { vm.stopPrank(); // Verify global FIFO order - (uint256[] memory globalIds, , , , , , , , , , ) = c.getPendingRequestsUnpacked(0, 0); + (uint256[] memory globalIds, , , , , , , , , ) = c.getPendingRequestsUnpacked(0, 0); assertEq(globalIds.length, 2); assertEq(globalIds[0], req1, "First should be req1"); assertEq(globalIds[1], req3, "Second should be req3"); // Verify user's array - (uint256[] memory userIds, , , , , , , , , , , ) = c.getPendingRequestsByUserUnpacked(user); + (uint256[] memory userIds, , , , , , , , , , ) = c.getPendingRequestsByUserUnpacked(user); assertEq(userIds.length, 2); }