Skip to content
Merged
2 changes: 1 addition & 1 deletion .github/copilot-instructions.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ flow test cadence/tests/<file>.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

Expand Down
4 changes: 2 additions & 2 deletions .github/workflows/claude-code-review.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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("<!-- claude-code-review -->")))] | length')"
'[.[] | select((.user.login == "claude[bot]" or .user.login == "claude") and (.body | contains("<!-- claude-code-review -->")))] | 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
2 changes: 1 addition & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`

Expand Down
8 changes: 3 additions & 5 deletions FLOW_YIELD_VAULTS_EVM_BRIDGE_DESIGN.md
Original file line number Diff line number Diff line change
Expand Up @@ -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) │ │
│ └───────────────────────────────────────────────────────────────────────┘ │
Expand Down Expand Up @@ -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?
Expand Down Expand Up @@ -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.
Expand Down
2 changes: 1 addition & 1 deletion FRONTEND_INTEGRATION.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
}
`;
Expand Down
2 changes: 1 addition & 1 deletion TESTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -252,7 +252,7 @@ access_control_test.cdc: 7 tests PASS
- testRequestsAddressCanBeUpdated
- testWorkerCreationRequiresCOA
- testWorkerCreationRequiresBetaBadge
- testYieldVaultsByEVMAddressMapping
- testYieldVaultRegistryMapping

error_handling_test.cdc: 4 tests PASS
- testInvalidRequestType
Expand Down
66 changes: 31 additions & 35 deletions cadence/contracts/FlowYieldVaultsEVM.cdc
Original file line number Diff line number Diff line change
Expand Up @@ -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?
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand All @@ -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,
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -1637,22 +1629,27 @@ 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)
/// @param evmAddress The EVM address string to check
/// @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
Expand Down Expand Up @@ -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()
Expand Down
2 changes: 1 addition & 1 deletion cadence/scripts/check_yieldvault_details.cdc
Original file line number Diff line number Diff line change
Expand Up @@ -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] = []
Expand Down
2 changes: 1 addition & 1 deletion cadence/scripts/check_yieldvaultmanager_status.cdc
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
12 changes: 6 additions & 6 deletions cadence/scripts/get_contract_state.cdc
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
4 changes: 2 additions & 2 deletions cadence/tests/access_control_test.cdc
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down