diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index 92b365f..6ec6fc1 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -47,7 +47,7 @@ flow test cadence/tests/.cdc # Single test (after deps installed) Both contracts maintain parallel ownership mappings for O(1) lookups: - Solidity: `userOwnsYieldVault[address][yieldVaultId]` -- Cadence: `yieldVaultOwnershipLookup[evmAddrString][yieldVaultId]` +- Cadence: `yieldVaultRegistry[evmAddrString][yieldVaultId]` ### COA Bridge Pattern diff --git a/.github/workflows/claude-code-review.yml b/.github/workflows/claude-code-review.yml index 5b503fe..ba1f421 100644 --- a/.github/workflows/claude-code-review.yml +++ b/.github/workflows/claude-code-review.yml @@ -74,9 +74,9 @@ jobs: pr='${{ github.event.pull_request.number }}' count="$(gh api "repos/${repo}/issues/${pr}/comments" --paginate --jq \ - '[.[] | select(.user.login == "claude[bot]" and (.body | contains("")))] | length')" + '[.[] | select((.user.login == "claude[bot]" or .user.login == "claude") and (.body | contains("")))] | length')" if [ "${count}" -lt 1 ]; then - echo "::error::No Claude sticky review comment found (claude[bot] + marker)." + echo "::error::No Claude sticky review comment found (claude or claude[bot] + marker)." exit 1 fi diff --git a/CLAUDE.md b/CLAUDE.md index a010fa7..ae0f9b5 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -62,7 +62,7 @@ flow deps install --skip-alias --skip-deployments # Install dependencies - **COA Bridge**: Cadence Owned Account bridges funds between EVM and Cadence via FlowEVMBridge - **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 +- **Ownership Tracking**: Parallel mappings on both EVM (`userOwnsYieldVault`) and Cadence (`yieldVaultRegistry`) for O(1) lookups - **Adaptive Scheduling**: TransactionHandler adjusts delay based on pending count (3s for >10, 5s for >=5, 7s for >=1, 30s idle) - **Dynamic Execution Effort**: `baseEffortPerRequest * maxRequestsPerTx + baseOverhead` diff --git a/FLOW_YIELD_VAULTS_EVM_BRIDGE_DESIGN.md b/FLOW_YIELD_VAULTS_EVM_BRIDGE_DESIGN.md index 5242c33..f471d64 100644 --- a/FLOW_YIELD_VAULTS_EVM_BRIDGE_DESIGN.md +++ b/FLOW_YIELD_VAULTS_EVM_BRIDGE_DESIGN.md @@ -54,8 +54,7 @@ EVM users deposit FLOW and submit requests to a Solidity contract. A Cadence wor │ │ └─────────────────────────────────────────────────────────────────┘ │ │ │ │ │ │ │ │ State: │ │ -│ │ - yieldVaultsByEVMAddress: {String: [UInt64]} │ │ -│ │ - yieldVaultOwnershipLookup: {String: {UInt64: Bool}} │ │ +│ │ - yieldVaultRegistry: {String: {UInt64: Bool}} │ │ │ │ - flowYieldVaultsRequestsAddress: EVM.EVMAddress? │ │ │ │ - maxRequestsPerTx: Int (default: 1) │ │ │ └───────────────────────────────────────────────────────────────────────┘ │ @@ -132,8 +131,7 @@ Worker contract that processes EVM requests and manages YieldVault positions. **Key State:** ```cadence // YieldVault ownership tracking -access(all) let yieldVaultsByEVMAddress: {String: [UInt64]} -access(all) let yieldVaultOwnershipLookup: {String: {UInt64: Bool}} +access(all) let yieldVaultRegistry: {String: {UInt64: Bool}} // Configuration (stored as contract-only vars; exposed via getters) var flowYieldVaultsRequestsAddress: EVM.EVMAddress? @@ -571,7 +569,7 @@ mapping(address => mapping(uint64 => bool)) public userOwnsYieldVault; ```cadence // Cadence -access(all) let yieldVaultOwnershipLookup: {String: {UInt64: Bool}} +access(all) let yieldVaultRegistry: {String: {UInt64: Bool}} ``` Ownership is verified for WITHDRAW/CLOSE on both EVM and Cadence. Deposits are permissionless; CREATE only validates identifiers. diff --git a/FRONTEND_INTEGRATION.md b/FRONTEND_INTEGRATION.md index 140d465..312bc2a 100644 --- a/FRONTEND_INTEGRATION.md +++ b/FRONTEND_INTEGRATION.md @@ -503,7 +503,7 @@ import FlowYieldVaultsEVM from 0xdf111ffc5064198a access(all) fun main(): {String: AnyStruct} { return { "flowYieldVaultsRequestsAddress": FlowYieldVaultsEVM.getFlowYieldVaultsRequestsAddress()?.toString() ?? "not set", - "totalEVMUsers": FlowYieldVaultsEVM.yieldVaultsByEVMAddress.keys.length + "totalEVMUsers": FlowYieldVaultsEVM.yieldVaultRegistry.keys.length } } `; diff --git a/TESTING.md b/TESTING.md index 77f8b1c..be8fb50 100644 --- a/TESTING.md +++ b/TESTING.md @@ -252,7 +252,7 @@ access_control_test.cdc: 7 tests PASS - testRequestsAddressCanBeUpdated - testWorkerCreationRequiresCOA - testWorkerCreationRequiresBetaBadge -- testYieldVaultsByEVMAddressMapping +- testYieldVaultRegistryMapping error_handling_test.cdc: 4 tests PASS - testInvalidRequestType diff --git a/cadence/contracts/FlowYieldVaultsEVM.cdc b/cadence/contracts/FlowYieldVaultsEVM.cdc index 240c651..17b7723 100644 --- a/cadence/contracts/FlowYieldVaultsEVM.cdc +++ b/cadence/contracts/FlowYieldVaultsEVM.cdc @@ -163,13 +163,10 @@ access(all) contract FlowYieldVaultsEVM { /// @notice Storage path for Admin resource access(all) let AdminStoragePath: StoragePath - /// @notice YieldVault Ids owned by each EVM address - /// @dev Maps EVM address string to array of owned YieldVault Ids for public queries - access(all) let yieldVaultsByEVMAddress: {String: [UInt64]} - - /// @notice O(1) lookup for yieldvault ownership verification + /// @notice Registry of EVM addresses and their owned yield vault IDs + /// Allows O(1) lookup for yield vault ownership verification /// @dev Maps EVM address string to {yieldVaultId: true} for fast ownership checks - access(all) let yieldVaultOwnershipLookup: {String: {UInt64: Bool}} + access(all) let yieldVaultRegistry: {String: {UInt64: Bool}} /// @notice Address of the FlowYieldVaultsRequests contract on EVM access(contract) var flowYieldVaultsRequestsAddress: EVM.EVMAddress? @@ -730,7 +727,7 @@ access(all) contract FlowYieldVaultsEVM { /// 2. Withdraws funds from COA (bridging ERC20 if needed) /// 3. Validates vault type matches the requested vaultIdentifier /// 4. Creates YieldVault via YieldVaultManager - /// 5. Records ownership in yieldVaultsByEVMAddress and yieldVaultOwnershipLookup + /// 5. Records ownership in yieldVaultRegistry /// @param request The CREATE_YIELDVAULT request containing vault/strategy identifiers and amount /// @return ProcessResult with success status, created yieldVaultId, and status message access(self) fun processCreateYieldVault(_ request: EVMRequest): ProcessResult { @@ -790,17 +787,11 @@ access(all) contract FlowYieldVaultsEVM { // Phase 5: Record ownership in contract state for O(1) lookups let evmAddr = request.user.toString() - // Initialize array for this address if needed - if FlowYieldVaultsEVM.yieldVaultsByEVMAddress[evmAddr] == nil { - FlowYieldVaultsEVM.yieldVaultsByEVMAddress[evmAddr] = [] - } - FlowYieldVaultsEVM.yieldVaultsByEVMAddress[evmAddr]!.append(yieldVaultId) - // Initialize ownership map for this address if needed - if FlowYieldVaultsEVM.yieldVaultOwnershipLookup[evmAddr] == nil { - FlowYieldVaultsEVM.yieldVaultOwnershipLookup[evmAddr] = {} + if FlowYieldVaultsEVM.yieldVaultRegistry[evmAddr] == nil { + FlowYieldVaultsEVM.yieldVaultRegistry[evmAddr] = {} } - FlowYieldVaultsEVM.yieldVaultOwnershipLookup[evmAddr]!.insert(key: yieldVaultId, true) + let _ = FlowYieldVaultsEVM.yieldVaultRegistry[evmAddr]!.insert(key: yieldVaultId, true) emit YieldVaultCreatedForEVMUser( requestId: request.id, @@ -832,8 +823,8 @@ access(all) contract FlowYieldVaultsEVM { let evmAddr = request.user.toString() // Step 1: Validate user ownership of the YieldVault - if let ownershipMap = FlowYieldVaultsEVM.yieldVaultOwnershipLookup[evmAddr] { - if ownershipMap[request.yieldVaultId] != true { + if let ownershipMap = FlowYieldVaultsEVM.yieldVaultRegistry[evmAddr] { + if !ownershipMap.containsKey(request.yieldVaultId) { return ProcessResult( success: false, yieldVaultId: request.yieldVaultId, @@ -855,11 +846,12 @@ access(all) contract FlowYieldVaultsEVM { // Step 3: Bridge funds back to user's EVM address self.bridgeFundsToEVMUser(vault: <-vault, recipient: request.user, tokenAddress: request.tokenAddress) - // Step 4: Remove yieldVaultId from ownership tracking - if let index = FlowYieldVaultsEVM.yieldVaultsByEVMAddress[evmAddr]!.firstIndex(of: request.yieldVaultId) { - let _ = FlowYieldVaultsEVM.yieldVaultsByEVMAddress[evmAddr]!.remove(at: index) + // Step 4: Remove yieldVaultId from registry mapping + let _ = FlowYieldVaultsEVM.yieldVaultRegistry[evmAddr]!.remove(key: request.yieldVaultId) + // Clean up empty dictionaries to optimize storage costs + if FlowYieldVaultsEVM.yieldVaultRegistry[evmAddr]!.length == 0 { + let _ = FlowYieldVaultsEVM.yieldVaultRegistry.remove(key: evmAddr) } - FlowYieldVaultsEVM.yieldVaultOwnershipLookup[evmAddr]!.remove(key: request.yieldVaultId) emit YieldVaultClosedForEVMUser( requestId: request.id, @@ -920,10 +912,10 @@ access(all) contract FlowYieldVaultsEVM { let betaRef = self.getBetaRef() self.getYieldVaultManagerRef().depositToYieldVault(betaRef: betaRef, request.yieldVaultId, from: <-vault) - // Check if depositor is the owner for event emission + // Check if depositor is the yield vault owner for event emission var isYieldVaultOwner = false - if let ownershipMap = FlowYieldVaultsEVM.yieldVaultOwnershipLookup[evmAddr] { - isYieldVaultOwner = ownershipMap[request.yieldVaultId] ?? false + if let ownershipMap = FlowYieldVaultsEVM.yieldVaultRegistry[evmAddr] { + isYieldVaultOwner = ownershipMap.containsKey(request.yieldVaultId) } emit YieldVaultDepositedForEVMUser( requestId: request.id, @@ -954,8 +946,8 @@ access(all) contract FlowYieldVaultsEVM { let evmAddr = request.user.toString() // Step 1: Validate user ownership of the YieldVault - if let ownershipMap = FlowYieldVaultsEVM.yieldVaultOwnershipLookup[evmAddr] { - if ownershipMap[request.yieldVaultId] != true { + if let ownershipMap = FlowYieldVaultsEVM.yieldVaultRegistry[evmAddr] { + if !ownershipMap.containsKey(request.yieldVaultId) { return ProcessResult( success: false, yieldVaultId: request.yieldVaultId, @@ -1637,11 +1629,15 @@ access(all) contract FlowYieldVaultsEVM { // Public Functions // ============================================ - /// @notice Gets all YieldVault Ids owned by an EVM address + /// @notice Gets all YieldVault Ids registered to an EVM address /// @param evmAddress The EVM address string to query - /// @return Array of YieldVault Ids owned by the address + /// @return Array of YieldVault Ids owned by the address (order is not guaranteed) access(all) view fun getYieldVaultIdsForEVMAddress(_ evmAddress: String): [UInt64] { - return self.yieldVaultsByEVMAddress[evmAddress] ?? [] + if !self.yieldVaultRegistry.containsKey(evmAddress) { + return [] + } + + return self.yieldVaultRegistry[evmAddress]!.keys } /// @notice Checks if an EVM address owns a specific YieldVault Id (O(1) lookup) @@ -1649,10 +1645,11 @@ access(all) contract FlowYieldVaultsEVM { /// @param yieldVaultId The YieldVault Id to verify ownership of /// @return True if the address owns the YieldVault, false otherwise access(all) view fun doesEVMAddressOwnYieldVault(evmAddress: String, yieldVaultId: UInt64): Bool { - if let ownershipMap = self.yieldVaultOwnershipLookup[evmAddress] { - return ownershipMap[yieldVaultId] ?? false + if !self.yieldVaultRegistry.containsKey(evmAddress) { + return false } - return false + + return self.yieldVaultRegistry[evmAddress]!.containsKey(yieldVaultId) } /// @notice Gets the configured FlowYieldVaultsRequests contract address @@ -1946,8 +1943,7 @@ access(all) contract FlowYieldVaultsEVM { self.WorkerStoragePath = /storage/flowYieldVaultsEVM self.AdminStoragePath = /storage/flowYieldVaultsEVMAdmin self.maxRequestsPerTx = 1 - self.yieldVaultsByEVMAddress = {} - self.yieldVaultOwnershipLookup = {} + self.yieldVaultRegistry = {} self.flowYieldVaultsRequestsAddress = nil let admin <- create Admin() diff --git a/cadence/scripts/check_yieldvault_details.cdc b/cadence/scripts/check_yieldvault_details.cdc index d487bf4..028256a 100644 --- a/cadence/scripts/check_yieldvault_details.cdc +++ b/cadence/scripts/check_yieldvault_details.cdc @@ -12,7 +12,7 @@ access(all) fun main(account: Address): {String: AnyStruct} { result["contractAddress"] = account.toString() result["flowYieldVaultsRequestsAddress"] = FlowYieldVaultsEVM.getFlowYieldVaultsRequestsAddress()?.toString() ?? "not set" - let yieldVaultsByEVM = FlowYieldVaultsEVM.yieldVaultsByEVMAddress + let yieldVaultsByEVM = FlowYieldVaultsEVM.yieldVaultRegistry result["totalEVMAddresses"] = yieldVaultsByEVM.keys.length let allYieldVaultIds: [UInt64] = [] diff --git a/cadence/scripts/check_yieldvaultmanager_status.cdc b/cadence/scripts/check_yieldvaultmanager_status.cdc index d396e0d..6133af6 100644 --- a/cadence/scripts/check_yieldvaultmanager_status.cdc +++ b/cadence/scripts/check_yieldvaultmanager_status.cdc @@ -20,7 +20,7 @@ access(all) fun main(accountAddress: Address): {String: AnyStruct} { paths["yieldVaultManagerPublic"] = FlowYieldVaults.YieldVaultManagerPublicPath.toString() result["paths"] = paths - let yieldVaultsByEVM = FlowYieldVaultsEVM.yieldVaultsByEVMAddress + let yieldVaultsByEVM = FlowYieldVaultsEVM.yieldVaultRegistry result["totalEVMAddresses"] = yieldVaultsByEVM.keys.length var totalYieldVaultsMapped = 0 diff --git a/cadence/scripts/get_contract_state.cdc b/cadence/scripts/get_contract_state.cdc index 8591c9c..a10a9a5 100644 --- a/cadence/scripts/get_contract_state.cdc +++ b/cadence/scripts/get_contract_state.cdc @@ -10,25 +10,25 @@ access(all) fun main(contractAddress: Address): {String: AnyStruct} { result["flowYieldVaultsRequestsAddress"] = FlowYieldVaultsEVM.getFlowYieldVaultsRequestsAddress()?.toString() ?? "Not set" result["maxRequestsPerTx"] = FlowYieldVaultsEVM.getMaxRequestsPerTx() - result["yieldVaultsByEVMAddress"] = FlowYieldVaultsEVM.yieldVaultsByEVMAddress + result["yieldVaultRegistry"] = FlowYieldVaultsEVM.yieldVaultRegistry result["WorkerStoragePath"] = FlowYieldVaultsEVM.WorkerStoragePath.toString() result["AdminStoragePath"] = FlowYieldVaultsEVM.AdminStoragePath.toString() var totalYieldVaults = 0 var totalEVMAddresses = 0 - for evmAddress in FlowYieldVaultsEVM.yieldVaultsByEVMAddress.keys { + for evmAddress in FlowYieldVaultsEVM.yieldVaultRegistry.keys { totalEVMAddresses = totalEVMAddresses + 1 - let yieldVaultIds = FlowYieldVaultsEVM.yieldVaultsByEVMAddress[evmAddress]! - totalYieldVaults = totalYieldVaults + yieldVaultIds.length + let yieldVaultOwnershipMap = FlowYieldVaultsEVM.yieldVaultRegistry[evmAddress]! + totalYieldVaults = totalYieldVaults + yieldVaultOwnershipMap.keys.length } result["totalEVMAddresses"] = totalEVMAddresses result["totalYieldVaults"] = totalYieldVaults let evmAddressDetails: {String: Int} = {} - for evmAddress in FlowYieldVaultsEVM.yieldVaultsByEVMAddress.keys { - evmAddressDetails[evmAddress] = FlowYieldVaultsEVM.yieldVaultsByEVMAddress[evmAddress]!.length + for evmAddress in FlowYieldVaultsEVM.yieldVaultRegistry.keys { + evmAddressDetails[evmAddress] = FlowYieldVaultsEVM.yieldVaultRegistry[evmAddress]!.length } result["evmAddressDetails"] = evmAddressDetails diff --git a/cadence/tests/access_control_test.cdc b/cadence/tests/access_control_test.cdc index 0df96f0..adec22a 100644 --- a/cadence/tests/access_control_test.cdc +++ b/cadence/tests/access_control_test.cdc @@ -118,8 +118,8 @@ fun testWorkerCreationRequiresBetaBadge() { } access(all) -fun testYieldVaultsByEVMAddressMapping() { - // Verify the yieldVaultsByEVMAddress mapping is accessible +fun testYieldVaultRegistryMapping() { + // Verify the yieldVaultRegistry mapping is accessible let testAddress = "0x6666666666666666666666666666666666666666" let yieldVaultIds = FlowYieldVaultsEVM.getYieldVaultIdsForEVMAddress(testAddress)