From b06eeb2efc8b46a6d273c553eec483f0d8036fb4 Mon Sep 17 00:00:00 2001 From: liobrasil Date: Wed, 11 Mar 2026 02:34:13 -0400 Subject: [PATCH 1/5] fix: handle unordered close results in syWFLOWvStrategy --- .../contracts/FlowYieldVaultsStrategiesV2.cdc | 47 +++++++++++-------- 1 file changed, 27 insertions(+), 20 deletions(-) diff --git a/cadence/contracts/FlowYieldVaultsStrategiesV2.cdc b/cadence/contracts/FlowYieldVaultsStrategiesV2.cdc index 060b4301..9630eccd 100644 --- a/cadence/contracts/FlowYieldVaultsStrategiesV2.cdc +++ b/cadence/contracts/FlowYieldVaultsStrategiesV2.cdc @@ -745,34 +745,41 @@ access(all) contract FlowYieldVaultsStrategiesV2 { message: "Expected 1 or 2 vaults from closePosition, got \(resultVaults.length)" ) - var collateralVault <- resultVaults.removeFirst() - assert( - collateralVault.getType() == internalCollateralType, - message: "First vault returned from closePosition must be internal collateral (\(internalCollateralType.identifier)), got \(collateralVault.getType().identifier)" - ) - - // Handle any overpayment dust (FLOW) returned as the second vault. + // closePosition returns vaults in dict-iteration order, so do not assume the + // collateral vault is first. Identify it by type and route any remaining vault + // as overpayment dust. + var collateralVault <- DeFiActionsUtils.getEmptyVault(internalCollateralType) + var foundCollateral = false while resultVaults.length > 0 { - let dustVault <- resultVaults.removeFirst() - if dustVault.balance > 0.0 { - if dustVault.getType() == internalCollateralType { - collateralVault.deposit(from: <-dustVault) + let returnedVault <- resultVaults.removeFirst() + if returnedVault.getType() == internalCollateralType { + foundCollateral = true + collateralVault.deposit(from: <-returnedVault) + } else if returnedVault.balance > 0.0 { + // Handle overpayment dust (FLOW) returned by closePosition. + let quote = self.debtToCollateralSwapper.quoteOut( + forProvided: returnedVault.balance, + reverse: false + ) + if quote.outAmount > 0.0 { + let swapped <- self.debtToCollateralSwapper.swap( + quote: quote, + inVault: <-returnedVault + ) + collateralVault.deposit(from: <-swapped) } else { - // Quote first — if dust is too small to route, destroy it - let quote = self.debtToCollateralSwapper.quoteOut(forProvided: dustVault.balance, reverse: false) - if quote.outAmount > 0.0 { - let swapped <- self.debtToCollateralSwapper.swap(quote: quote, inVault: <-dustVault) - collateralVault.deposit(from: <-swapped) - } else { - Burner.burn(<-dustVault) - } + Burner.burn(<-returnedVault) } } else { - Burner.burn(<-dustVault) + Burner.burn(<-returnedVault) } } destroy resultVaults + assert( + foundCollateral, + message: "closePosition did not return internal collateral of type \(internalCollateralType.identifier)" + ) // Convert internal collateral (MOET) → external collateral (e.g. PYUSD0) if needed if internalCollateralType != collateralType { From 3f72eca92f2474dd611e3208a9b5037a63ed410c Mon Sep 17 00:00:00 2001 From: liobrasil Date: Wed, 11 Mar 2026 02:35:10 -0400 Subject: [PATCH 2/5] chore: wire syWFLOWv v2 into mainnet setup --- local/setup_mainnet.sh | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/local/setup_mainnet.sh b/local/setup_mainnet.sh index da45c1ca..b352be9b 100755 --- a/local/setup_mainnet.sh +++ b/local/setup_mainnet.sh @@ -197,9 +197,11 @@ flow transactions send ./cadence/transactions/flow-yield-vaults/admin/add_strate --network mainnet \ --signer mainnet-admin -# configure syWFLOWvStrategy (MoreERC4626) collateral configs +# configure FlowYieldVaultsStrategiesV2 syWFLOWv strategy configs # -# PYUSD0: yieldToUnderlying = syWFLOWv→WFLOW (fee 100), debtToCollateral = WFLOW→PYUSD0 (fee 500) +# syWFLOWv -> WFLOW is the same for all collaterals (UniV3 fee 100). +# WFLOW -> collateral differs per collateral type. +# PYUSD0 uses a direct WFLOW -> PYUSD0 route (fee 500). flow transactions send ./cadence/transactions/flow-yield-vaults/admin/upsert_more_erc4626_config.cdc \ 'A.b1d63873c3cc9f79.FlowYieldVaultsStrategiesV2.syWFLOWvStrategy' \ 'A.1e4aa0b87d10b141.EVMVMBridgedToken_99af3eea856556646c98c8b9b2548fe815240750.Vault' \ @@ -211,11 +213,12 @@ flow transactions send ./cadence/transactions/flow-yield-vaults/admin/upsert_mor --network mainnet \ --signer mainnet-admin -# MOET pre-swap: PYUSD0→MOET via UniV3 fee 100 (FlowALP only accepts MOET as stablecoin collateral) +# Configure PYUSD0 -> MOET pre-swap for syWFLOWvStrategy. The strategy internally uses +# MOET as FlowALP collateral, so incoming PYUSD0 must be swapped before opening/updating positions. flow transactions send ./cadence/transactions/flow-yield-vaults/admin/upsert_moet_preswap_config.cdc \ 'A.b1d63873c3cc9f79.FlowYieldVaultsStrategiesV2.MoreERC4626StrategyComposer' \ 'A.1e4aa0b87d10b141.EVMVMBridgedToken_99af3eea856556646c98c8b9b2548fe815240750.Vault' \ - '["0x99aF3EeA856556646C98c8B9b2548Fe815240750","0x213979bb8a9a86966999b3aa797c1fcf3b967ae2"]' \ + '["0x99aF3EeA856556646C98c8B9b2548Fe815240750","0x213979bB8A9A86966999b3AA797C1fcf3B967ae2"]' \ '[100]' \ --network mainnet \ --signer mainnet-admin From 17b81e0ed313475a0f611cc0f84082a5d1d88c9f Mon Sep 17 00:00:00 2001 From: liobrasil Date: Wed, 11 Mar 2026 11:11:25 -0400 Subject: [PATCH 3/5] fix: clean up FUSDEV burn-time swapper state --- cadence/contracts/FlowYieldVaultsStrategiesV2.cdc | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/cadence/contracts/FlowYieldVaultsStrategiesV2.cdc b/cadence/contracts/FlowYieldVaultsStrategiesV2.cdc index 9630eccd..31a16042 100644 --- a/cadence/contracts/FlowYieldVaultsStrategiesV2.cdc +++ b/cadence/contracts/FlowYieldVaultsStrategiesV2.cdc @@ -497,6 +497,8 @@ access(all) contract FlowYieldVaultsStrategiesV2 { FlowYieldVaultsStrategiesV2._removeOriginalCollateralType(id.id) FlowYieldVaultsStrategiesV2._removeCollateralPreSwapper(id.id) FlowYieldVaultsStrategiesV2._removeMoetToCollateralSwapper(id.id) + FlowYieldVaultsStrategiesV2._removeYieldToMoetSwapper(id.id) + FlowYieldVaultsStrategiesV2._removeCollateralToDebtSwapper(id.id) FlowYieldVaultsStrategiesV2._removeDebtToCollateralSwapper(id.id) } } @@ -2090,6 +2092,12 @@ access(all) contract FlowYieldVaultsStrategiesV2 { FlowYieldVaultsStrategiesV2.config["yieldToMoetSwappers"] = partition } + access(contract) fun _removeYieldToMoetSwapper(_ id: UInt64) { + var partition = FlowYieldVaultsStrategiesV2.config["yieldToMoetSwappers"] as! {UInt64: {DeFiActions.Swapper}}? ?? {} + partition.remove(key: id) + FlowYieldVaultsStrategiesV2.config["yieldToMoetSwappers"] = partition + } + // --- "debtToCollateralSwappers" partition --- access(contract) view fun _getDebtToCollateralSwapper(_ id: UInt64): {DeFiActions.Swapper}? { @@ -2125,6 +2133,12 @@ access(all) contract FlowYieldVaultsStrategiesV2 { FlowYieldVaultsStrategiesV2.config["collateralToDebtSwappers"] = partition } + access(contract) fun _removeCollateralToDebtSwapper(_ id: UInt64) { + var partition = FlowYieldVaultsStrategiesV2.config["collateralToDebtSwappers"] as! {UInt64: {DeFiActions.Swapper}}? ?? {} + partition.remove(key: id) + FlowYieldVaultsStrategiesV2.config["collateralToDebtSwappers"] = partition + } + // --- "closedPositions" partition --- access(contract) view fun _isPositionClosed(_ uniqueID: DeFiActions.UniqueIdentifier?): Bool { From c7685faca9083e49845bcb2aea9e589e8b167dad Mon Sep 17 00:00:00 2001 From: liobrasil Date: Wed, 11 Mar 2026 11:15:23 -0400 Subject: [PATCH 4/5] fix: harden FUSDEV close-position return handling --- .../contracts/FlowYieldVaultsStrategiesV2.cdc | 118 +++++++++++++----- 1 file changed, 89 insertions(+), 29 deletions(-) diff --git a/cadence/contracts/FlowYieldVaultsStrategiesV2.cdc b/cadence/contracts/FlowYieldVaultsStrategiesV2.cdc index 31a16042..130cdd3d 100644 --- a/cadence/contracts/FlowYieldVaultsStrategiesV2.cdc +++ b/cadence/contracts/FlowYieldVaultsStrategiesV2.cdc @@ -323,6 +323,14 @@ access(all) contract FlowYieldVaultsStrategiesV2 { result.getType() == collateralType: "Withdraw Vault (\(result.getType().identifier)) is not of a requested collateral type (\(collateralType.identifier))" } + // Determine the internal collateral type (may be MOET if a pre-swap was applied). + var internalCollateralType = collateralType + if let id = self.uniqueID { + if FlowYieldVaultsStrategiesV2._getOriginalCollateralType(id.id) != nil { + internalCollateralType = self.sink.getSinkType() + } + } + // Step 1: Get debt amounts - returns {Type: UFix64} dictionary let debtsByType = self.position.getTotalDebt() @@ -332,6 +340,11 @@ access(all) contract FlowYieldVaultsStrategiesV2 { message: "FUSDEVStrategy position must have at most one debt type, found \(debtsByType.length)" ) + var debtType: Type? = nil + for currentDebtType in debtsByType.keys { + debtType = currentDebtType + } + // Step 2: Calculate total debt amount var totalDebtAmount: UFix64 = 0.0 for debtAmount in debtsByType.values { @@ -358,21 +371,27 @@ access(all) contract FlowYieldVaultsStrategiesV2 { } var collateralVault <- resultVaults.removeFirst() destroy resultVaults + assert( + collateralVault.getType() == internalCollateralType, + message: "closePosition returned unexpected collateral type \(collateralVault.getType().identifier); expected \(internalCollateralType.identifier)" + ) // Convert internal collateral (MOET) → external collateral (e.g. PYUSD0) if needed - if let id = self.uniqueID { - if let moetToOrigSwapper = FlowYieldVaultsStrategiesV2._getMoetToCollateralSwapper(id.id) { - if collateralVault.balance > 0.0 { - let quote = moetToOrigSwapper.quoteOut(forProvided: collateralVault.balance, reverse: false) - if quote.outAmount > 0.0 { - let extVault <- moetToOrigSwapper.swap(quote: quote, inVault: <-collateralVault) - FlowYieldVaultsStrategiesV2._markPositionClosed(self.uniqueID) - return <- extVault + if internalCollateralType != collateralType { + if let id = self.uniqueID { + if let moetToOrigSwapper = FlowYieldVaultsStrategiesV2._getMoetToCollateralSwapper(id.id) { + if collateralVault.balance > 0.0 { + let quote = moetToOrigSwapper.quoteOut(forProvided: collateralVault.balance, reverse: false) + if quote.outAmount > 0.0 { + let extVault <- moetToOrigSwapper.swap(quote: quote, inVault: <-collateralVault) + FlowYieldVaultsStrategiesV2._markPositionClosed(self.uniqueID) + return <- extVault + } } } - Burner.burn(<-collateralVault) - FlowYieldVaultsStrategiesV2._markPositionClosed(self.uniqueID) - return <- DeFiActionsUtils.getEmptyVault(collateralType) } + Burner.burn(<-collateralVault) + FlowYieldVaultsStrategiesV2._markPositionClosed(self.uniqueID) + return <- DeFiActionsUtils.getEmptyVault(collateralType) } FlowYieldVaultsStrategiesV2._markPositionClosed(self.uniqueID) return <- collateralVault @@ -454,37 +473,78 @@ access(all) contract FlowYieldVaultsStrategiesV2 { // Step 8: Close position - pool pulls up to the (now pre-reduced) debt from moetSource let resultVaults <- self.position.closePosition(repaymentSources: [moetSource]) - // With one collateral type and one debt type, the pool returns at most two vaults: - // the collateral vault and optionally a MOET overpayment dust vault. - // closePosition returns vaults in dict-iteration order (hash-based), so we cannot - // assume the collateral vault is first. Find it by type and convert any non-collateral - // vaults (MOET overpayment dust) back to collateral via the stored swapper. + assert( + resultVaults.length >= 1 && resultVaults.length <= 2, + message: "Expected 1 or 2 vaults from closePosition, got \(resultVaults.length)" + ) + + // With one collateral type and one debt type, closePosition returns the internal + // collateral vault and may also return a debt-token overpayment dust vault. + // In the stablecoin pre-swap path, the internal collateral is also MOET, so both + // returned vaults may share the same token type. Aggregate every internal-collateral + // vault; any remaining non-empty vault must match the debt type and is routed back + // into collateral via the stored debt→collateral swapper. let debtToCollateralSwapper = FlowYieldVaultsStrategiesV2._getDebtToCollateralSwapper(self.uniqueID!.id) - var collateralVault <- DeFiActionsUtils.getEmptyVault(collateralType) + var collateralVault <- DeFiActionsUtils.getEmptyVault(internalCollateralType) + var foundCollateral = false while resultVaults.length > 0 { - let v <- resultVaults.removeFirst() - if v.getType() == collateralType { - collateralVault.deposit(from: <-v) - } else if v.balance > 0.0 { - if let swapper = debtToCollateralSwapper { - // Quote first — if dust is too small to route, destroy it - let quote = swapper.quoteOut(forProvided: v.balance, reverse: false) + let returnedVault <- resultVaults.removeFirst() + if returnedVault.getType() == internalCollateralType { + foundCollateral = true + collateralVault.deposit(from: <-returnedVault) + } else { + let expectedDebtType = debtType + ?? panic( + "FUSDEVStrategy closePosition returned non-collateral vault \(returnedVault.getType().identifier) with no recorded debt type" + ) + assert( + returnedVault.getType() == expectedDebtType, + message: "closePosition returned unexpected vault type \(returnedVault.getType().identifier); expected \(expectedDebtType.identifier)" + ) + if returnedVault.balance > 0.0 { + let swapper = debtToCollateralSwapper + ?? panic( + "No debt→collateral swapper found for non-zero \(returnedVault.getType().identifier) dust" + ) + // Quote first — if dust is too small to route, destroy it. + let quote = swapper.quoteOut(forProvided: returnedVault.balance, reverse: false) if quote.outAmount > 0.0 { - let swapped <- swapper.swap(quote: quote, inVault: <-v) + let swapped <- swapper.swap(quote: quote, inVault: <-returnedVault) collateralVault.deposit(from: <-swapped) } else { - Burner.burn(<-v) + Burner.burn(<-returnedVault) } } else { - Burner.burn(<-v) + Burner.burn(<-returnedVault) } - } else { - Burner.burn(<-v) } } destroy resultVaults + assert( + foundCollateral, + message: "closePosition did not return internal collateral of type \(internalCollateralType.identifier)" + ) + + if internalCollateralType != collateralType { + if let id = self.uniqueID { + if let moetToOrigSwapper = FlowYieldVaultsStrategiesV2._getMoetToCollateralSwapper(id.id) { + if collateralVault.balance > 0.0 { + let quote = moetToOrigSwapper.quoteOut(forProvided: collateralVault.balance, reverse: false) + if quote.outAmount > 0.0 { + let extVault <- moetToOrigSwapper.swap(quote: quote, inVault: <-collateralVault) + FlowYieldVaultsStrategiesV2._markPositionClosed(self.uniqueID) + return <- extVault + } + } + } + } + Burner.burn(<-collateralVault) + FlowYieldVaultsStrategiesV2._markPositionClosed(self.uniqueID) + return <- DeFiActionsUtils.getEmptyVault(collateralType) + } + FlowYieldVaultsStrategiesV2._markPositionClosed(self.uniqueID) return <- collateralVault } From b3aac6037194a0f8c1c5716f474b1b26cacae95b Mon Sep 17 00:00:00 2001 From: liobrasil Date: Wed, 11 Mar 2026 11:22:17 -0400 Subject: [PATCH 5/5] docs: tighten syWFLOWv mainnet setup comments --- local/setup_mainnet.sh | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/local/setup_mainnet.sh b/local/setup_mainnet.sh index b352be9b..153c7d03 100755 --- a/local/setup_mainnet.sh +++ b/local/setup_mainnet.sh @@ -213,8 +213,9 @@ flow transactions send ./cadence/transactions/flow-yield-vaults/admin/upsert_mor --network mainnet \ --signer mainnet-admin -# Configure PYUSD0 -> MOET pre-swap for syWFLOWvStrategy. The strategy internally uses -# MOET as FlowALP collateral, so incoming PYUSD0 must be swapped before opening/updating positions. +# Configure PYUSD0 -> MOET pre-swap for syWFLOWvStrategy. FlowALP only accepts +# MOET as stablecoin collateral, so incoming PYUSD0 must be swapped before opening +# or updating positions. flow transactions send ./cadence/transactions/flow-yield-vaults/admin/upsert_moet_preswap_config.cdc \ 'A.b1d63873c3cc9f79.FlowYieldVaultsStrategiesV2.MoreERC4626StrategyComposer' \ 'A.1e4aa0b87d10b141.EVMVMBridgedToken_99af3eea856556646c98c8b9b2548fe815240750.Vault' \ @@ -223,7 +224,8 @@ flow transactions send ./cadence/transactions/flow-yield-vaults/admin/upsert_moe --network mainnet \ --signer mainnet-admin -# WBTC: no WFLOW/WBTC pool — use 2-hop WFLOW→WETH→WBTC (fees 3000/3000) +# WBTC uses a 2-hop WFLOW -> WETH -> WBTC route (fees 3000/3000) because no +# direct WFLOW/WBTC pool is configured here. flow transactions send ./cadence/transactions/flow-yield-vaults/admin/upsert_more_erc4626_config.cdc \ 'A.b1d63873c3cc9f79.FlowYieldVaultsStrategiesV2.syWFLOWvStrategy' \ 'A.1e4aa0b87d10b141.EVMVMBridgedToken_717dae2baf7656be9a9b01dee31d571a9d4c9579.Vault' \ @@ -235,7 +237,7 @@ flow transactions send ./cadence/transactions/flow-yield-vaults/admin/upsert_mor --network mainnet \ --signer mainnet-admin -# WETH: yieldToUnderlying = syWFLOWv→WFLOW (fee 100), debtToCollateral = WFLOW→WETH (fee 3000) +# WETH uses a direct WFLOW -> WETH route (fee 3000). flow transactions send ./cadence/transactions/flow-yield-vaults/admin/upsert_more_erc4626_config.cdc \ 'A.b1d63873c3cc9f79.FlowYieldVaultsStrategiesV2.syWFLOWvStrategy' \ 'A.1e4aa0b87d10b141.EVMVMBridgedToken_2f6f07cdcf3588944bf4c42ac74ff24bf56e7590.Vault' \