From 90aa86fadf4c6040ce14193279bd33edd206aef9 Mon Sep 17 00:00:00 2001 From: Alex <12097569+nialexsan@users.noreply.github.com> Date: Mon, 16 Mar 2026 11:03:41 -0400 Subject: [PATCH 01/29] isolated strategy contract from fyv-v2-strategy-deployment --- .../contracts/FlowYieldVaultsStrategiesV2.cdc | 1937 ++++++++++++++--- 1 file changed, 1597 insertions(+), 340 deletions(-) diff --git a/cadence/contracts/FlowYieldVaultsStrategiesV2.cdc b/cadence/contracts/FlowYieldVaultsStrategiesV2.cdc index 7ee81669..22e19374 100644 --- a/cadence/contracts/FlowYieldVaultsStrategiesV2.cdc +++ b/cadence/contracts/FlowYieldVaultsStrategiesV2.cdc @@ -1,4 +1,5 @@ // standards +import "Burner" import "FungibleToken" import "EVM" // DeFiActions @@ -42,11 +43,90 @@ access(all) contract FlowYieldVaultsStrategiesV2 { access(all) let univ3RouterEVMAddress: EVM.EVMAddress access(all) let univ3QuoterEVMAddress: EVM.EVMAddress + /// Partitioned config map. Each key is a partition name; each value is a typed nested map keyed by + /// strategy UniqueIdentifier ID (UInt64). Current partitions: + /// "yieldToMoetSwappers" → {UInt64: {DeFiActions.Swapper}} + /// "debtToCollateralSwappers" → {UInt64: {DeFiActions.Swapper}} + /// "collateralToDebtSwappers" → {UInt64: {DeFiActions.Swapper}} + /// "closedPositions" → {UInt64: Bool} + /// "syWFLOWvDebtTokenTypes" → {UInt64: Type} + /// "moreERC4626Configs" → {Type: {Type: {Type: MoreERC4626CollateralConfig}}} + /// "moetPreswapConfigs" → {Type: {Type: MoetPreswapConfig}} + /// "originalCollateralTypes" → {UInt64: Type} + /// "collateralPreSwappers" → {UInt64: {DeFiActions.Swapper}} + /// "moetToCollateralSwappers" → {UInt64: {DeFiActions.Swapper}} access(contract) let config: {String: AnyStruct} /// Canonical StoragePath where the StrategyComposerIssuer should be stored access(all) let IssuerStoragePath: StoragePath + /// A Source that converts yield tokens to debt tokens by pulling ALL available yield + /// tokens from the wrapped source, rather than using quoteIn to limit the pull amount. + /// + /// This avoids ERC4626 rounding issues where quoteIn might underestimate required shares, + /// causing the swap to return less than the requested debt amount. By pulling everything + /// and swapping everything, the output is as large as the yield position allows. + /// + /// The caller is responsible for ensuring the yield tokens (after swapping) will cover the + /// required debt — e.g. by pre-depositing supplemental MOET to reduce the position's debt + /// before calling closePosition (see FUSDEVStrategy.closePosition step 6). + access(all) struct BufferedSwapSource : DeFiActions.Source { + access(self) let swapper: {DeFiActions.Swapper} + access(self) let source: {DeFiActions.Source} + access(contract) var uniqueID: DeFiActions.UniqueIdentifier? + + init( + swapper: {DeFiActions.Swapper}, + source: {DeFiActions.Source}, + uniqueID: DeFiActions.UniqueIdentifier? + ) { + pre { + source.getSourceType() == swapper.inType(): + "source type != swapper inType" + } + self.swapper = swapper + self.source = source + self.uniqueID = uniqueID + } + + access(all) fun getComponentInfo(): DeFiActions.ComponentInfo { + return DeFiActions.ComponentInfo( + type: self.getType(), + id: self.id(), + innerComponents: [ + self.swapper.getComponentInfo(), + self.source.getComponentInfo() + ] + ) + } + access(contract) view fun copyID(): DeFiActions.UniqueIdentifier? { return self.uniqueID } + access(contract) fun setID(_ id: DeFiActions.UniqueIdentifier?) { self.uniqueID = id } + access(all) view fun getSourceType(): Type { return self.swapper.outType() } + access(all) fun minimumAvailable(): UFix64 { + let avail = self.source.minimumAvailable() + if avail == 0.0 { return 0.0 } + return self.swapper.quoteOut(forProvided: avail, reverse: false).outAmount + } + /// Pulls ALL available yield tokens from the source and swaps them to the debt token. + /// Ignores quoteIn — avoids ERC4626 rounding underestimates that would leave us short. + access(FungibleToken.Withdraw) fun withdrawAvailable(maxAmount: UFix64): @{FungibleToken.Vault} { + if maxAmount == 0.0 { + return <- DeFiActionsUtils.getEmptyVault(self.getSourceType()) + } + let availableIn = self.source.minimumAvailable() + if availableIn == 0.0 { + return <- DeFiActionsUtils.getEmptyVault(self.getSourceType()) + } + // Pull ALL available yield tokens (not quoteIn-limited) + let sourceLiquidity <- self.source.withdrawAvailable(maxAmount: availableIn) + if sourceLiquidity.balance == 0.0 { + Burner.burn(<-sourceLiquidity) + return <- DeFiActionsUtils.getEmptyVault(self.getSourceType()) + } + return <- self.swapper.swap(quote: nil, inVault: <-sourceLiquidity) + } + } + access(all) struct CollateralConfig { access(all) let yieldTokenEVMAddress: EVM.EVMAddress access(all) let yieldToCollateralUniV3AddressPath: [EVM.EVMAddress] @@ -72,6 +152,66 @@ access(all) contract FlowYieldVaultsStrategiesV2 { } } + /// Configuration for pre-swapping a stablecoin collateral to MOET before depositing into + /// FlowALP. Required when the collateral type is not directly supported by FlowALP (e.g. + /// PYUSD0 must be swapped to MOET since FlowALP only supports MOET as its stablecoin). + /// + /// The path is collateral → MOET (e.g. [PYUSD0_addr, MOET_addr] for a 1-hop swap, or + /// [PYUSD0_addr, WFLOW_addr, MOET_addr] for a 2-hop swap). The reverse (MOET→collateral) + /// is derived automatically by reversing both arrays. + access(all) struct MoetPreswapConfig { + /// Full UniV3 swap path: collateral EVM address → ... → MOET EVM address. + /// First element is the collateral, last element must be MOET. + access(all) let collateralToMoetAddressPath: [EVM.EVMAddress] + /// UniV3 fee tiers for each hop (length must equal addressPath.length - 1). + access(all) let collateralToMoetFeePath: [UInt32] + + init( + collateralToMoetAddressPath: [EVM.EVMAddress], + collateralToMoetFeePath: [UInt32] + ) { + pre { + collateralToMoetAddressPath.length > 1: + "MoetPreswapConfig: path must have at least 2 elements (collateral + MOET)" + collateralToMoetFeePath.length == collateralToMoetAddressPath.length - 1: + "MoetPreswapConfig: fee path length must equal address path length - 1" + } + self.collateralToMoetAddressPath = collateralToMoetAddressPath + self.collateralToMoetFeePath = collateralToMoetFeePath + } + } + + /// Collateral configuration for strategies that borrow the vault's underlying asset directly, + /// using a standard ERC4626 deposit for the forward path (underlying → yield token) and a + /// UniV3 AMM swap for the reverse path (yield token → underlying). This applies to "More" + /// ERC4626 vaults that do not support synchronous redemptions via ERC4626 redeem(). + access(all) struct MoreERC4626CollateralConfig { + access(all) let yieldTokenEVMAddress: EVM.EVMAddress + /// UniV3 path for swapping yield token → underlying asset (used for debt repayment and + /// AutoBalancer rebalancing). The path must start with the yield token EVM address. + access(all) let yieldToUnderlyingUniV3AddressPath: [EVM.EVMAddress] + access(all) let yieldToUnderlyingUniV3FeePath: [UInt32] + /// UniV3 path for swapping debt token → collateral (used to convert overpayment dust + /// returned by position.closePosition back into collateral). The path must start with + /// the debt token EVM address and end with the collateral EVM address. + access(all) let debtToCollateralUniV3AddressPath: [EVM.EVMAddress] + access(all) let debtToCollateralUniV3FeePath: [UInt32] + + init( + yieldTokenEVMAddress: EVM.EVMAddress, + yieldToUnderlyingUniV3AddressPath: [EVM.EVMAddress], + yieldToUnderlyingUniV3FeePath: [UInt32], + debtToCollateralUniV3AddressPath: [EVM.EVMAddress], + debtToCollateralUniV3FeePath: [UInt32] + ) { + self.yieldTokenEVMAddress = yieldTokenEVMAddress + self.yieldToUnderlyingUniV3AddressPath = yieldToUnderlyingUniV3AddressPath + self.yieldToUnderlyingUniV3FeePath = yieldToUnderlyingUniV3FeePath + self.debtToCollateralUniV3AddressPath = debtToCollateralUniV3AddressPath + self.debtToCollateralUniV3FeePath = debtToCollateralUniV3FeePath + } + } + /// This strategy uses FUSDEV vault (Morpho ERC4626). /// Deposits collateral into a single FlowALP position, borrowing MOET as debt. /// MOET is swapped to PYUSD0 and deposited into the Morpho FUSDEV ERC4626 vault. @@ -84,10 +224,6 @@ access(all) contract FlowYieldVaultsStrategiesV2 { access(self) let position: @FlowALPv0.Position access(self) var sink: {DeFiActions.Sink} access(self) var source: {DeFiActions.Source} - /// Tracks whether the underlying FlowALP position has been closed. Once true, - /// availableBalance() returns 0.0 to avoid panicking when the pool no longer - /// holds the position (e.g. during YieldVault burnCallback after close). - access(self) var positionClosed: Bool init( id: DeFiActions.UniqueIdentifier, @@ -97,7 +233,6 @@ access(all) contract FlowYieldVaultsStrategiesV2 { self.uniqueID = id self.sink = position.createSink(type: collateralType) self.source = position.createSourceWithOptions(type: collateralType, pullFromTopUpSource: true) - self.positionClosed = false self.position <-position } @@ -105,20 +240,59 @@ access(all) contract FlowYieldVaultsStrategiesV2 { // access(all) view fun isSupportedCollateralType(_ type: Type): Bool access(all) view fun getSupportedCollateralTypes(): {Type: Bool} { + // If this strategy was initialized with a stablecoin pre-swap (e.g. PYUSD0→MOET), + // expose the original (external) collateral type to callers, not the internal MOET type. + if let id = self.uniqueID { + if let originalType = FlowYieldVaultsStrategiesV2._getOriginalCollateralType(id.id) { + return { originalType: true } + } + } return { self.sink.getSinkType(): true } } /// Returns the amount available for withdrawal via the inner Source access(all) fun availableBalance(ofToken: Type): UFix64 { - if self.positionClosed { return 0.0 } - return ofToken == self.source.getSourceType() ? self.source.minimumAvailable() : 0.0 + if FlowYieldVaultsStrategiesV2._isPositionClosed(self.uniqueID) { return 0.0 } + // If stablecoin pre-swap is in effect, match against the original (external) collateral type. + // MOET and PYUSD0 are both stablecoins with approximately equal value (1:1), so the MOET + // balance is a reasonable approximation of the PYUSD0-denominated collateral balance. + var effectiveSourceType = self.source.getSourceType() + if let id = self.uniqueID { + if let originalType = FlowYieldVaultsStrategiesV2._getOriginalCollateralType(id.id) { + effectiveSourceType = originalType + } + } + return ofToken == effectiveSourceType ? self.source.minimumAvailable() : 0.0 } /// Deposits up to the inner Sink's capacity from the provided authorized Vault reference. - /// Only the single configured collateral type is accepted — one collateral type per position. + /// Accepts both the internal collateral type (MOET) and, when a pre-swap is configured, + /// the original external collateral type (e.g. PYUSD0) — which is swapped to MOET first. access(all) fun deposit(from: auth(FungibleToken.Withdraw) &{FungibleToken.Vault}) { pre { - from.getType() == self.sink.getSinkType(): + from.getType() == self.sink.getSinkType() + || (self.uniqueID != nil && FlowYieldVaultsStrategiesV2._getOriginalCollateralType(self.uniqueID!.id) == from.getType()): "FUSDEVStrategy position only accepts \(self.sink.getSinkType().identifier) as collateral, got \(from.getType().identifier)" } + // If depositing the original stablecoin collateral (e.g. PYUSD0), pre-swap to MOET + if let id = self.uniqueID { + if from.getType() != self.sink.getSinkType() { + if let preSwapper = FlowYieldVaultsStrategiesV2._getCollateralPreSwapper(id.id) { + let incoming <- from.withdraw(amount: from.balance) + if incoming.balance > 0.0 { + let quote = preSwapper.quoteOut(forProvided: incoming.balance, reverse: false) + if quote.outAmount > 0.0 { + let moetVault <- preSwapper.swap(quote: quote, inVault: <-incoming) + self.sink.depositCapacity(from: &moetVault as auth(FungibleToken.Withdraw) &{FungibleToken.Vault}) + Burner.burn(<-moetVault) + } else { + Burner.burn(<-incoming) + } + } else { + Burner.burn(<-incoming) + } + return + } + } + } self.sink.depositCapacity(from: from) } /// Withdraws up to the max amount, returning the withdrawn Vault. If the requested token type is unsupported, @@ -179,12 +353,28 @@ access(all) contract FlowYieldVaultsStrategiesV2 { // Zero vaults: dust collateral rounded down to zero — return an empty vault if resultVaults.length == 0 { destroy resultVaults - self.positionClosed = true + FlowYieldVaultsStrategiesV2._markPositionClosed(self.uniqueID) return <- DeFiActionsUtils.getEmptyVault(collateralType) } - let collateralVault <- resultVaults.removeFirst() + var collateralVault <- resultVaults.removeFirst() destroy resultVaults - self.positionClosed = true + // 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 + } + } + Burner.burn(<-collateralVault) + FlowYieldVaultsStrategiesV2._markPositionClosed(self.uniqueID) + return <- DeFiActionsUtils.getEmptyVault(collateralType) + } + } + FlowYieldVaultsStrategiesV2._markPositionClosed(self.uniqueID) return <- collateralVault } @@ -192,25 +382,364 @@ access(all) contract FlowYieldVaultsStrategiesV2 { let yieldTokenSource = FlowYieldVaultsAutoBalancers.createExternalSource(id: self.id()!) ?? panic("Could not create external source from AutoBalancer") - // Step 5: Retrieve yield→MOET swapper from contract config - let swapperKey = FlowYieldVaultsStrategiesV2.getYieldToMoetSwapperConfigKey(self.uniqueID)! - let yieldToMoetSwapper = FlowYieldVaultsStrategiesV2.config[swapperKey] as! {DeFiActions.Swapper}? + // Step 5: Retrieve yield→MOET swapper + let yieldToMoetSwapper = FlowYieldVaultsStrategiesV2._getYieldToMoetSwapper(self.uniqueID!.id) ?? panic("No yield→MOET swapper found for strategy \(self.id()!)") - // Step 6: Create a SwapSource that converts yield tokens to MOET when pulled by closePosition. - // The pool will call source.withdrawAvailable(maxAmount: debtAmount) which internally uses - // quoteIn(forDesired: debtAmount) to compute the exact yield token input needed. - let moetSource = SwapConnectors.SwapSource( + // Step 6: Pre-supplement from collateral if yield is insufficient to cover the full debt. + // + // The FUSDEV close path has a structural ~0.02% round-trip fee loss: + // Open: MOET → PYUSD0 (UniV3 0.01%) → FUSDEV (ERC4626, free) + // Close: FUSDEV → PYUSD0 (ERC4626, free) → MOET (UniV3 0.01%) + // In production, accrued yield more than covers this; with no accrued yield (e.g. in + // tests, immediate open+close), the yield tokens convert back to slightly less MOET + // than was borrowed. We handle this by pre-pulling a tiny amount of collateral from + // self.source, swapping it to MOET, and depositing it into the position to reduce the + // outstanding debt — BEFORE calling position.closePosition. + // + // This MUST be done before closePosition because the position is locked during close: + // any attempt to pull from self.source inside a repaymentSource.withdrawAvailable call + // would trigger "Reentrancy: position X is locked". + let yieldAvail = yieldTokenSource.minimumAvailable() + let expectedMOET = yieldAvail > 0.0 + ? yieldToMoetSwapper.quoteOut(forProvided: yieldAvail, reverse: false).outAmount + : 0.0 + if expectedMOET < totalDebtAmount { + if let collateralToMoetSwapper = FlowYieldVaultsStrategiesV2._getCollateralToDebtSwapper(self.uniqueID!.id) { + let shortfall = totalDebtAmount - expectedMOET + // Add 1% buffer to account for swap slippage/rounding in the collateral→MOET leg + let buffered = shortfall + shortfall / 100.0 + let quote = collateralToMoetSwapper.quoteIn(forDesired: buffered, reverse: false) + if quote.inAmount > 0.0 { + let extraCollateral <- self.source.withdrawAvailable(maxAmount: quote.inAmount) + if extraCollateral.balance > 0.0 { + let extraMOET <- collateralToMoetSwapper.swap(quote: quote, inVault: <-extraCollateral) + if extraMOET.balance > 0.0 { + // Deposit MOET to reduce position debt before close + self.position.deposit(from: <-extraMOET) + } else { + Burner.burn(<-extraMOET) + } + } else { + Burner.burn(<-extraCollateral) + } + } + } else { + if let id = self.uniqueID { + if FlowYieldVaultsStrategiesV2._getOriginalCollateralType(id.id) != nil { + // Stablecoin pre-swap case: position collateral IS MOET (same as debt token). + // Pull MOET directly from the collateral source to pre-reduce debt — no swap needed. + let shortfall = totalDebtAmount - expectedMOET + let buffered = shortfall + shortfall / 100.0 + let extraMOET <- self.source.withdrawAvailable(maxAmount: buffered) + if extraMOET.balance > 0.0 { + self.position.deposit(from: <-extraMOET) + } else { + Burner.burn(<-extraMOET) + } + } + } + } + } + + // Step 7: Create a BufferedSwapSource that converts ALL yield tokens → MOET. + // Pulling all (not quoteIn-limited) avoids ERC4626 rounding underestimates. + // After the pre-supplement above, the remaining debt is covered by the yield tokens. + let moetSource = FlowYieldVaultsStrategiesV2.BufferedSwapSource( swapper: yieldToMoetSwapper, source: yieldTokenSource, uniqueID: self.copyID() ) - // Step 7: Close position - pool pulls exactly the debt amount from moetSource + // 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. + let debtToCollateralSwapper = FlowYieldVaultsStrategiesV2._getDebtToCollateralSwapper(self.uniqueID!.id) + + var collateralVault <- DeFiActionsUtils.getEmptyVault(collateralType) + 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) + if quote.outAmount > 0.0 { + let swapped <- swapper.swap(quote: quote, inVault: <-v) + collateralVault.deposit(from: <-swapped) + } else { + Burner.burn(<-v) + } + } else { + Burner.burn(<-v) + } + } else { + Burner.burn(<-v) + } + } + + destroy resultVaults + FlowYieldVaultsStrategiesV2._markPositionClosed(self.uniqueID) + return <- collateralVault + } + /// Executed when a Strategy is burned, cleaning up the Strategy's stored AutoBalancer + access(contract) fun burnCallback() { + FlowYieldVaultsAutoBalancers._cleanupAutoBalancer(id: self.id()!) + FlowYieldVaultsStrategiesV2._cleanupPositionClosed(self.uniqueID) + // Clean up stablecoin pre-swap config entries (no-op if not set) + if let id = self.uniqueID { + FlowYieldVaultsStrategiesV2._removeOriginalCollateralType(id.id) + FlowYieldVaultsStrategiesV2._removeCollateralPreSwapper(id.id) + FlowYieldVaultsStrategiesV2._removeMoetToCollateralSwapper(id.id) + FlowYieldVaultsStrategiesV2._removeDebtToCollateralSwapper(id.id) + } + } + access(all) fun getComponentInfo(): DeFiActions.ComponentInfo { + return DeFiActions.ComponentInfo( + type: self.getType(), + id: self.id(), + innerComponents: [ + self.sink.getComponentInfo(), + self.source.getComponentInfo() + ] + ) + } + access(contract) view fun copyID(): DeFiActions.UniqueIdentifier? { + return self.uniqueID + } + access(contract) fun setID(_ id: DeFiActions.UniqueIdentifier?) { + self.uniqueID = id + } + } + + /// This strategy uses syWFLOWv vault (More ERC4626). + /// Deposits collateral (non-FLOW) into a single FlowALP position, borrowing FLOW as debt. + /// Borrowed FLOW is deposited directly into the syWFLOWv More ERC4626 vault (no AMM swap needed + /// since FLOW is the vault's underlying asset). + /// FLOW (the vault's underlying asset) cannot be used as collateral for this strategy. + access(all) resource syWFLOWvStrategy : FlowYieldVaults.Strategy, DeFiActions.IdentifiableResource { + /// An optional identifier allowing protocols to identify stacked connector operations by defining a protocol- + /// specific Identifier to associated connectors on construction + access(contract) var uniqueID: DeFiActions.UniqueIdentifier? + access(self) let position: @FlowALPv0.Position + access(self) var sink: {DeFiActions.Sink} + access(self) var source: {DeFiActions.Source} + // debtTokenType moved to contract-level config["syWFLOWvDebtTokenTypes"] keyed by uniqueID.id + /// Swapper used in closePosition to source FLOW from the AutoBalancer (syWFLOWv → FLOW via UniV3). + access(self) let yieldToDebtSwapper: {DeFiActions.Swapper} + /// Swapper used in closePosition to convert FLOW overpayment dust back to collateral (FLOW → collateral via UniV3). + access(self) let debtToCollateralSwapper: {DeFiActions.Swapper} + /// Tracks whether the underlying FlowALP position has been closed. Once true, + /// availableBalance() returns 0.0 to avoid panicking when the pool no longer + /// holds the position (e.g. during YieldVault burnCallback after close). + access(self) var positionClosed: Bool + + init( + id: DeFiActions.UniqueIdentifier, + collateralType: Type, + position: @FlowALPv0.Position, + yieldToDebtSwapper: {DeFiActions.Swapper}, + debtToCollateralSwapper: {DeFiActions.Swapper} + ) { + self.uniqueID = id + self.sink = position.createSink(type: collateralType) + self.source = position.createSourceWithOptions(type: collateralType, pullFromTopUpSource: true) + self.yieldToDebtSwapper = yieldToDebtSwapper + self.debtToCollateralSwapper = debtToCollateralSwapper + self.positionClosed = false + self.position <-position + } + + // Inherited from FlowYieldVaults.Strategy default implementation + // access(all) view fun isSupportedCollateralType(_ type: Type): Bool + + access(all) view fun getSupportedCollateralTypes(): {Type: Bool} { + // If this strategy was initialized with a stablecoin pre-swap (e.g. PYUSD0→MOET), + // expose the original (external) collateral type to callers, not the internal MOET type. + if let id = self.uniqueID { + if let originalType = FlowYieldVaultsStrategiesV2._getOriginalCollateralType(id.id) { + return { originalType: true } + } + } + return { self.sink.getSinkType(): true } + } + /// Returns the amount available for withdrawal via the inner Source + access(all) fun availableBalance(ofToken: Type): UFix64 { + if self.positionClosed { return 0.0 } + // If stablecoin pre-swap is in effect, match against the original (external) collateral type. + // MOET and the stablecoin collateral (e.g. PYUSD0) have approximately equal value (1:1). + var effectiveSourceType = self.source.getSourceType() + if let id = self.uniqueID { + if let originalType = FlowYieldVaultsStrategiesV2._getOriginalCollateralType(id.id) { + effectiveSourceType = originalType + } + } + return ofToken == effectiveSourceType ? self.source.minimumAvailable() : 0.0 + } + /// Deposits up to the inner Sink's capacity from the provided authorized Vault reference. + /// Accepts both the internal collateral type and, when a pre-swap is configured, the original + /// external collateral type (e.g. PYUSD0) — which is swapped to MOET first. + /// FLOW cannot be used as collateral — it is the vault's underlying asset (the debt token). + access(all) fun deposit(from: auth(FungibleToken.Withdraw) &{FungibleToken.Vault}) { + pre { + from.getType() == self.sink.getSinkType() + || (self.uniqueID != nil && FlowYieldVaultsStrategiesV2._getOriginalCollateralType(self.uniqueID!.id) == from.getType()): + "syWFLOWvStrategy position only accepts \(self.sink.getSinkType().identifier) as collateral, got \(from.getType().identifier)" + } + // Reject the debt token (FLOW) as collateral — looked up from contract-level config + if let id = self.uniqueID { + let debtTokenType = FlowYieldVaultsStrategiesV2._getSyWFLOWvDebtTokenType(id.id) + assert( + debtTokenType == nil || from.getType() != debtTokenType!, + message: "syWFLOWvStrategy: FLOW cannot be used as collateral — it is the vault's underlying asset" + ) + } + // If depositing the original stablecoin collateral (e.g. PYUSD0), pre-swap to MOET + if let id = self.uniqueID { + if from.getType() != self.sink.getSinkType() { + if let preSwapper = FlowYieldVaultsStrategiesV2._getCollateralPreSwapper(id.id) { + let incoming <- from.withdraw(amount: from.balance) + if incoming.balance > 0.0 { + let quote = preSwapper.quoteOut(forProvided: incoming.balance, reverse: false) + if quote.outAmount > 0.0 { + let moetVault <- preSwapper.swap(quote: quote, inVault: <-incoming) + self.sink.depositCapacity(from: &moetVault as auth(FungibleToken.Withdraw) &{FungibleToken.Vault}) + Burner.burn(<-moetVault) + } else { + Burner.burn(<-incoming) + } + } else { + Burner.burn(<-incoming) + } + return + } + } + } + self.sink.depositCapacity(from: from) + } + /// Withdraws up to the max amount, returning the withdrawn Vault. If the requested token type is unsupported, + /// an empty Vault is returned. + /// + /// For the PYUSD0 pre-swap case: the internal source type is MOET but the external collateral + /// type is PYUSD0. We convert MOET→PYUSD0 via the moetToCollateralSwapper. + access(FungibleToken.Withdraw) fun withdraw(maxAmount: UFix64, ofToken: Type): @{FungibleToken.Vault} { + let effectiveSourceType = self.source.getSourceType() + if ofToken != effectiveSourceType { + // For pre-swap case: ofToken is external collateral (e.g. PYUSD0), source is MOET. + if let id = self.uniqueID { + if let originalType = FlowYieldVaultsStrategiesV2._getOriginalCollateralType(id.id) { + if ofToken == originalType { + if let moetToOrigSwapper = FlowYieldVaultsStrategiesV2._getMoetToCollateralSwapper(id.id) { + // Quote MOET in to get maxAmount PYUSD0 out + let quote = moetToOrigSwapper.quoteIn(forDesired: maxAmount, reverse: false) + if quote.inAmount > 0.0 { + let moetVault <- self.source.withdrawAvailable(maxAmount: quote.inAmount) + if moetVault.balance > 0.0 { + return <- moetToOrigSwapper.swap(quote: nil, inVault: <-moetVault) + } + Burner.burn(<-moetVault) + } + } + } + } + } + return <- DeFiActionsUtils.getEmptyVault(ofToken) + } + return <- self.source.withdrawAvailable(maxAmount: maxAmount) + } + /// Closes the underlying FlowALP position by preparing FLOW repayment funds from AutoBalancer + /// (via the stored yield→FLOW swapper) and closing with them. When a stablecoin pre-swap is + /// configured (e.g. PYUSD0→MOET), the returned MOET collateral is swapped back to the + /// original stablecoin before returning. + access(FungibleToken.Withdraw) fun closePosition(collateralType: Type): @{FungibleToken.Vault} { + pre { + self.isSupportedCollateralType(collateralType): + "Unsupported collateral type \(collateralType.identifier)" + } + post { + 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() // MOET + } + } + + // Step 1: Get debt amounts + let debtsByType = self.position.getTotalDebt() + + assert( + debtsByType.length <= 1, + message: "syWFLOWvStrategy position must have at most one debt type, found \(debtsByType.length)" + ) + + var totalDebtAmount: UFix64 = 0.0 + for debtAmount in debtsByType.values { + totalDebtAmount = totalDebtAmount + debtAmount + } + + // Step 2: If no debt, close with empty sources array + if totalDebtAmount == 0.0 { + let resultVaults <- self.position.closePosition(repaymentSources: []) + assert( + resultVaults.length <= 1, + message: "Expected 0 or 1 collateral vault from closePosition, got \(resultVaults.length)" + ) + if resultVaults.length == 0 { + destroy resultVaults + self.positionClosed = true + return <- DeFiActionsUtils.getEmptyVault(collateralType) + } + var collateralVault <- resultVaults.removeFirst() + destroy resultVaults + // Convert internal collateral (MOET) → external collateral (e.g. PYUSD0) if needed + 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) + self.positionClosed = true + return <- extVault + } + } + Burner.burn(<-collateralVault) + self.positionClosed = true + return <- DeFiActionsUtils.getEmptyVault(collateralType) + } + } + Burner.burn(<-collateralVault) + self.positionClosed = true + return <- DeFiActionsUtils.getEmptyVault(collateralType) + } + self.positionClosed = true + return <- collateralVault + } + + // Step 3: Create external syWFLOWv source from AutoBalancer + let yieldTokenSource = FlowYieldVaultsAutoBalancers.createExternalSource(id: self.id()!) + ?? panic("Could not create external source from AutoBalancer") + + // Step 4: Create a SwapSource that converts syWFLOWv → FLOW for debt repayment + let flowSource = SwapConnectors.SwapSource( + swapper: self.yieldToDebtSwapper, + source: yieldTokenSource, + uniqueID: self.copyID() + ) + + // Step 5: Close position — pool pulls exactly the FLOW debt amount from flowSource + let resultVaults <- self.position.closePosition(repaymentSources: [flowSource]) + assert( resultVaults.length >= 1 && resultVaults.length <= 2, message: "Expected 1 or 2 vaults from closePosition, got \(resultVaults.length)" @@ -218,32 +747,65 @@ access(all) contract FlowYieldVaultsStrategiesV2 { var collateralVault <- resultVaults.removeFirst() assert( - collateralVault.getType() == collateralType, - message: "First vault returned from closePosition must be collateral (\(collateralType.identifier)), got \(collateralVault.getType().identifier)" + collateralVault.getType() == internalCollateralType, + message: "First vault returned from closePosition must be internal collateral (\(internalCollateralType.identifier)), got \(collateralVault.getType().identifier)" ) - // Handle any overpayment dust (MOET) returned as the second vault + // Handle any overpayment dust (FLOW) returned as the second vault. while resultVaults.length > 0 { let dustVault <- resultVaults.removeFirst() if dustVault.balance > 0.0 { - if dustVault.getType() == collateralType { + if dustVault.getType() == internalCollateralType { collateralVault.deposit(from: <-dustVault) } else { - // @TODO implement swapping moet to collateral - destroy dustVault + // 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) + } } } else { - destroy dustVault + Burner.burn(<-dustVault) } } destroy resultVaults + + // Convert internal collateral (MOET) → external collateral (e.g. PYUSD0) if needed + 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) + self.positionClosed = true + return <- extVault + } + } + } + } + Burner.burn(<-collateralVault) + self.positionClosed = true + return <- DeFiActionsUtils.getEmptyVault(collateralType) + } + self.positionClosed = true return <- collateralVault } - /// Executed when a Strategy is burned, cleaning up the Strategy's stored AutoBalancer + /// Executed when a Strategy is burned, cleaning up the Strategy's stored AutoBalancer and contract-level config entries access(contract) fun burnCallback() { FlowYieldVaultsAutoBalancers._cleanupAutoBalancer(id: self.id()!) + FlowYieldVaultsStrategiesV2._removeSyWFLOWvDebtTokenType(self.uniqueID?.id) + // Clean up stablecoin pre-swap config entries (no-op if not set) + if let id = self.uniqueID { + FlowYieldVaultsStrategiesV2._removeOriginalCollateralType(id.id) + FlowYieldVaultsStrategiesV2._removeCollateralPreSwapper(id.id) + FlowYieldVaultsStrategiesV2._removeMoetToCollateralSwapper(id.id) + } } access(all) fun getComponentInfo(): DeFiActions.ComponentInfo { return DeFiActions.ComponentInfo( @@ -264,6 +826,7 @@ access(all) contract FlowYieldVaultsStrategiesV2 { } access(all) struct TokenBundle { + /// The MOET token type (the pool's borrowable token) access(all) let moetTokenType: Type access(all) let moetTokenEVMAddress: EVM.EVMAddress @@ -310,12 +873,116 @@ access(all) contract FlowYieldVaultsStrategiesV2 { } } - /// This StrategyComposer builds a Strategy that uses MorphoERC4626 vault + /* =========================== + Contract-level shared infrastructure + =========================== */ + + /// Gets the Pool's default token type (the borrowable token) + access(self) fun _getPoolDefaultToken(): Type { + let poolCap = FlowYieldVaultsStrategiesV2.account.storage.copy< + Capability + >(from: FlowALPv0.PoolCapStoragePath) + ?? panic("Missing or invalid pool capability") + let poolRef = poolCap.borrow() ?? panic("Invalid Pool Cap") + return poolRef.getDefaultToken() + } + + /// Resolves the full token bundle for a strategy given the ERC4626 yield vault address. + /// The MOET token is always the pool's default token. + access(self) fun _resolveTokenBundle(yieldTokenEVMAddress: EVM.EVMAddress): FlowYieldVaultsStrategiesV2.TokenBundle { + let moetTokenType = FlowYieldVaultsStrategiesV2._getPoolDefaultToken() + let moetTokenEVMAddress = FlowEVMBridgeConfig.getEVMAddressAssociated(with: moetTokenType) + ?? panic("Token Vault type \(moetTokenType.identifier) has not yet been registered with the VMbridge") + + let yieldTokenType = FlowEVMBridgeConfig.getTypeAssociated(with: yieldTokenEVMAddress) + ?? panic("Could not retrieve the VM Bridge associated Type for the yield token address \(yieldTokenEVMAddress.toString())") + + let underlying4626AssetEVMAddress = ERC4626Utils.underlyingAssetEVMAddress(vault: yieldTokenEVMAddress) + ?? panic("Could not get the underlying asset's EVM address for ERC4626Vault \(yieldTokenEVMAddress.toString())") + let underlying4626AssetType = FlowEVMBridgeConfig.getTypeAssociated(with: underlying4626AssetEVMAddress) + ?? panic("Could not retrieve the VM Bridge associated Type for the ERC4626 underlying asset \(underlying4626AssetEVMAddress.toString())") + + return FlowYieldVaultsStrategiesV2.TokenBundle( + moetTokenType: moetTokenType, + moetTokenEVMAddress: moetTokenEVMAddress, + yieldTokenType: yieldTokenType, + yieldTokenEVMAddress: yieldTokenEVMAddress, + underlying4626AssetType: underlying4626AssetType, + underlying4626AssetEVMAddress: underlying4626AssetEVMAddress + ) + } + + access(self) fun _createYieldTokenOracle( + yieldTokenEVMAddress: EVM.EVMAddress, + underlyingAssetType: Type, + uniqueID: DeFiActions.UniqueIdentifier + ): ERC4626PriceOracles.PriceOracle { + return ERC4626PriceOracles.PriceOracle( + vault: yieldTokenEVMAddress, + asset: underlyingAssetType, + uniqueID: uniqueID + ) + } + + access(self) fun _initAutoBalancerAndIO( + oracle: {DeFiActions.PriceOracle}, + yieldTokenType: Type, + recurringConfig: DeFiActions.AutoBalancerRecurringConfig?, + uniqueID: DeFiActions.UniqueIdentifier + ): FlowYieldVaultsStrategiesV2.AutoBalancerIO { + let autoBalancerRef = + FlowYieldVaultsAutoBalancers._initNewAutoBalancer( + oracle: oracle, + vaultType: yieldTokenType, + lowerThreshold: 0.95, + upperThreshold: 1.05, + rebalanceSink: nil, + rebalanceSource: nil, + recurringConfig: recurringConfig, + uniqueID: uniqueID + ) + + let sink = autoBalancerRef.createBalancerSink() + ?? panic("Could not retrieve Sink from AutoBalancer with id \(uniqueID.id)") + let source = autoBalancerRef.createBalancerSource() + ?? panic("Could not retrieve Source from AutoBalancer with id \(uniqueID.id)") + + return FlowYieldVaultsStrategiesV2.AutoBalancerIO( + autoBalancer: autoBalancerRef, + sink: sink, + source: source + ) + } + + access(self) fun _openCreditPosition( + funds: @{FungibleToken.Vault}, + issuanceSink: {DeFiActions.Sink}, + repaymentSource: {DeFiActions.Source} + ): @FlowALPv0.Position { + let poolCap = FlowYieldVaultsStrategiesV2.account.storage.copy< + Capability + >(from: FlowALPv0.PoolCapStoragePath) + ?? panic("Missing or invalid pool capability") + + let poolRef = poolCap.borrow() ?? panic("Invalid Pool Cap") + + let position <- poolRef.createPosition( + funds: <-funds, + issuanceSink: issuanceSink, + repaymentSource: repaymentSource, + pushToDrawDownSink: true + ) + + return <-position + } + + /// This StrategyComposer builds a Strategy that uses ERC4626 and MorphoERC4626 vaults. + /// Only handles FUSDEVStrategy (Morpho-based strategies that require UniV3 swap paths). access(all) resource MorphoERC4626StrategyComposer : FlowYieldVaults.StrategyComposer { - /// { Strategy Type: { Collateral Type: FlowYieldVaultsStrategiesV2.CollateralConfig } } - access(self) let config: {Type: {Type: FlowYieldVaultsStrategiesV2.CollateralConfig}} + /// { Strategy Type: { Collateral Type: CollateralConfig } } + access(self) let config: {Type: {Type: CollateralConfig}} - init(_ config: {Type: {Type: FlowYieldVaultsStrategiesV2.CollateralConfig}}) { + init(_ config: {Type: {Type: CollateralConfig}}) { self.config = config } @@ -331,7 +998,7 @@ access(all) contract FlowYieldVaultsStrategiesV2 { /// Returns the Vault types which can be used to initialize a given Strategy access(all) view fun getSupportedInitializationVaults(forStrategy: Type): {Type: Bool} { let supported: {Type: Bool} = {} - if let strategyConfig = &self.config[forStrategy] as &{Type: FlowYieldVaultsStrategiesV2.CollateralConfig}? { + if let strategyConfig = self.config[forStrategy] { for collateralType in strategyConfig.keys { supported[collateralType] = true } @@ -370,10 +1037,12 @@ access(all) contract FlowYieldVaultsStrategiesV2 { collateralType: collateralType ) - let tokens = self._resolveTokenBundle(collateralConfig: collateralConfig) + let tokens = FlowYieldVaultsStrategiesV2._resolveTokenBundle( + yieldTokenEVMAddress: collateralConfig.yieldTokenEVMAddress + ) // Oracle used by AutoBalancer (tracks NAV of ERC4626 vault) - let yieldTokenOracle = self._createYieldTokenOracle( + let yieldTokenOracle = FlowYieldVaultsStrategiesV2._createYieldTokenOracle( yieldTokenEVMAddress: tokens.yieldTokenEVMAddress, underlyingAssetType: tokens.underlying4626AssetType, uniqueID: uniqueID @@ -383,102 +1052,184 @@ access(all) contract FlowYieldVaultsStrategiesV2 { let recurringConfig = FlowYieldVaultsStrategiesV2._createRecurringConfig(withID: uniqueID) // Create/store/publish/register AutoBalancer (returns authorized ref) - let balancerIO = self._initAutoBalancerAndIO( + let balancerIO = FlowYieldVaultsStrategiesV2._initAutoBalancerAndIO( oracle: yieldTokenOracle, yieldTokenType: tokens.yieldTokenType, recurringConfig: recurringConfig, uniqueID: uniqueID ) - // Swappers: MOET <-> YIELD (YIELD is ERC4626 vault token) - let moetToYieldSwapper = self._createMoetToYieldSwapper(strategyType: type, tokens: tokens, uniqueID: uniqueID) + switch type { - let yieldToMoetSwapper = self._createYieldToMoetSwapper(strategyType: type, tokens: tokens, uniqueID: uniqueID) + // ----------------------------------------------------------------------- + // FUSDEVStrategy: borrows MOET from the FlowALP position, swaps to FUSDEV + // ----------------------------------------------------------------------- + case Type<@FUSDEVStrategy>(): + // Swappers: MOET <-> YIELD + let debtToYieldSwapper = self._createDebtToYieldSwapper(tokens: tokens, uniqueID: uniqueID) + let yieldToDebtSwapper = self._createYieldToDebtSwapper(tokens: tokens, uniqueID: uniqueID) + + // AutoBalancer-directed swap IO + let abaSwapSink = SwapConnectors.SwapSink( + swapper: debtToYieldSwapper, + sink: balancerIO.sink, + uniqueID: uniqueID + ) + let abaSwapSource = SwapConnectors.SwapSource( + swapper: yieldToDebtSwapper, + source: balancerIO.source, + uniqueID: uniqueID + ) - // AutoBalancer-directed swap IO - let abaSwapSink = SwapConnectors.SwapSink( - swapper: moetToYieldSwapper, - sink: balancerIO.sink, - uniqueID: uniqueID - ) - let abaSwapSource = SwapConnectors.SwapSource( - swapper: yieldToMoetSwapper, - source: balancerIO.source, - uniqueID: uniqueID - ) + // Store yield→MOET swapper for later access during closePosition + FlowYieldVaultsStrategiesV2._setYieldToMoetSwapper(uniqueID.id, yieldToDebtSwapper) + + // --- Stablecoin pre-swap path (e.g. PYUSD0 → MOET) --- + // When configured, swap collateral to MOET before depositing into FlowALP, since + // FlowALP only supports MOET as its stablecoin collateral (not PYUSD0 etc.). + if let preswapCfg = FlowYieldVaultsStrategiesV2._getMoetPreswapConfig( + composer: Type<@MorphoERC4626StrategyComposer>(), + collateral: collateralType + ) { + let preSwapper = self._createUniV3Swapper( + tokenPath: preswapCfg.collateralToMoetAddressPath, + feePath: preswapCfg.collateralToMoetFeePath, + inVault: collateralType, + outVault: tokens.moetTokenType, + uniqueID: uniqueID + ) + let preSwapQuote = preSwapper.quoteOut(forProvided: withFunds.balance, reverse: false) + let moetFunds <- preSwapper.swap(quote: preSwapQuote, inVault: <-withFunds) + + // Open FlowALPv0 position with MOET as collateral + let position <- FlowYieldVaultsStrategiesV2._openCreditPosition( + funds: <-moetFunds, + issuanceSink: abaSwapSink, + repaymentSource: abaSwapSource + ) + + // AutoBalancer rebalancing via MOET collateral: + // Overflow: sell FUSDEV → MOET → add to position collateral + // Deficit: pull MOET from position → buy FUSDEV + let positionSink = position.createSinkWithOptions(type: tokens.moetTokenType, pushToDrawDownSink: true) + let positionSwapSink = SwapConnectors.SwapSink( + swapper: yieldToDebtSwapper, // FUSDEV → MOET + sink: positionSink, + uniqueID: uniqueID + ) + let positionSource = position.createSourceWithOptions(type: tokens.moetTokenType, pullFromTopUpSource: false) + let positionSwapSource = SwapConnectors.SwapSource( + swapper: debtToYieldSwapper, // MOET → FUSDEV + source: positionSource, + uniqueID: uniqueID + ) + balancerIO.autoBalancer.setSink(positionSwapSink, updateSinkID: true) + balancerIO.autoBalancer.setSource(positionSwapSource, updateSourceID: true) + + // Store original collateral type (PYUSD0) and pre-swapper for deposit/close + FlowYieldVaultsStrategiesV2._setOriginalCollateralType(uniqueID.id, collateralType) + FlowYieldVaultsStrategiesV2._setCollateralPreSwapper(uniqueID.id, preSwapper) + + // MOET → collateral (e.g. PYUSD0): use the preswap path in reverse. + // Per design: "use the same swapper in reverse during close position". + let moetToOrigCollateral = self._createUniV3Swapper( + tokenPath: preswapCfg.collateralToMoetAddressPath.reverse(), + feePath: preswapCfg.collateralToMoetFeePath.reverse(), + inVault: tokens.moetTokenType, + outVault: collateralType, + uniqueID: uniqueID + ) + // Store under both partitions: moetToCollateralSwappers (for the no-debt close + // path) and debtToCollateralSwappers (for the regular close path MOET dust). + FlowYieldVaultsStrategiesV2._setMoetToCollateralSwapper(uniqueID.id, moetToOrigCollateral) + FlowYieldVaultsStrategiesV2._setDebtToCollateralSwapper(uniqueID.id, moetToOrigCollateral) + // Note: _setCollateralToDebtSwapper is NOT set for stablecoin (MOET) collateral. + // The MOET-direct pre-supplement path in closePosition handles this case. + + return <-create FUSDEVStrategy( + id: uniqueID, + collateralType: tokens.moetTokenType, + position: <-position + ) + } - // Open FlowALPv0 position - let position <- self._openCreditPosition( - funds: <-withFunds, - issuanceSink: abaSwapSink, - repaymentSource: abaSwapSource - ) + // --- Standard path (WBTC, WETH — directly supported by FlowALP) --- - // Position Sink/Source (only Sink needed here, Source stays inside Strategy impl) - let positionSink = position.createSinkWithOptions(type: collateralType, pushToDrawDownSink: true) + // Open FlowALPv0 position + let position <- FlowYieldVaultsStrategiesV2._openCreditPosition( + funds: <-withFunds, + issuanceSink: abaSwapSink, + repaymentSource: abaSwapSource + ) - // Yield -> Collateral swapper for recollateralization - let yieldToCollateralSwapper = self._createYieldToCollateralSwapper( - collateralConfig: collateralConfig, - yieldTokenEVMAddress: tokens.yieldTokenEVMAddress, - yieldTokenType: tokens.yieldTokenType, - collateralType: collateralType, - uniqueID: uniqueID - ) + // Position Sink/Source for collateral rebalancing + let positionSink = position.createSinkWithOptions(type: collateralType, pushToDrawDownSink: true) - let positionSwapSink = SwapConnectors.SwapSink( - swapper: yieldToCollateralSwapper, - sink: positionSink, - uniqueID: uniqueID - ) + // Yield -> Collateral swapper for recollateralization + let yieldToCollateralSwapper = self._createYieldToCollateralSwapper( + collateralConfig: collateralConfig, + yieldTokenEVMAddress: tokens.yieldTokenEVMAddress, + yieldTokenType: tokens.yieldTokenType, + collateralType: collateralType, + uniqueID: uniqueID + ) - // pullFromTopUpSource: false ensures Position maintains health buffer - // This prevents Position from being pushed to minHealth (1.1) limit - let positionSource = position.createSourceWithOptions( - type: collateralType, - pullFromTopUpSource: false // ← CONSERVATIVE: maintain safety buffer - ) + let positionSwapSink = SwapConnectors.SwapSink( + swapper: yieldToCollateralSwapper, + sink: positionSink, + uniqueID: uniqueID + ) - // Create Collateral -> Yield swapper (reverse of yieldToCollateralSwapper) - // Allows AutoBalancer to pull collateral, swap to yield token - let collateralToYieldSwapper = self._createCollateralToYieldSwapper( - collateralConfig: collateralConfig, - yieldTokenEVMAddress: tokens.yieldTokenEVMAddress, - yieldTokenType: tokens.yieldTokenType, - collateralType: collateralType, - uniqueID: uniqueID - ) + // pullFromTopUpSource: false ensures Position maintains health buffer + let positionSource = position.createSourceWithOptions( + type: collateralType, + pullFromTopUpSource: false + ) - // Create Position swap source for AutoBalancer deficit recovery - // When AutoBalancer value drops below deposits, pulls collateral from Position - let positionSwapSource = SwapConnectors.SwapSource( - swapper: collateralToYieldSwapper, - source: positionSource, - uniqueID: uniqueID - ) + // Collateral -> Yield swapper for AutoBalancer deficit recovery + let collateralToYieldSwapper = self._createCollateralToYieldSwapper( + collateralConfig: collateralConfig, + yieldTokenEVMAddress: tokens.yieldTokenEVMAddress, + yieldTokenType: tokens.yieldTokenType, + collateralType: collateralType, + uniqueID: uniqueID + ) - // Set AutoBalancer sink for overflow -> recollateralize - balancerIO.autoBalancer.setSink(positionSwapSink, updateSinkID: true) + let positionSwapSource = SwapConnectors.SwapSource( + swapper: collateralToYieldSwapper, + source: positionSource, + uniqueID: uniqueID + ) - // Set AutoBalancer source for deficit recovery -> pull from Position - balancerIO.autoBalancer.setSource(positionSwapSource, updateSourceID: true) + balancerIO.autoBalancer.setSink(positionSwapSink, updateSinkID: true) + balancerIO.autoBalancer.setSource(positionSwapSource, updateSourceID: true) - // Store yield→MOET swapper in contract config for later access during closePosition - let yieldToMoetSwapperKey = FlowYieldVaultsStrategiesV2.getYieldToMoetSwapperConfigKey(uniqueID)! - FlowYieldVaultsStrategiesV2.config[yieldToMoetSwapperKey] = yieldToMoetSwapper + // Store collateral→MOET swapper for pre-supplement in closePosition. + // Used to cover the ~0.02% round-trip fee shortfall when yield hasn't accrued. + let collateralToDebtSwapper = self._createCollateralToDebtSwapper( + collateralConfig: collateralConfig, + tokens: tokens, + collateralType: collateralType, + uniqueID: uniqueID + ) + FlowYieldVaultsStrategiesV2._setCollateralToDebtSwapper(uniqueID.id, collateralToDebtSwapper) + + // Store MOET→collateral swapper for dust conversion in closePosition. + // Chain: MOET → FUSDEV (debtToYieldSwapper) → collateral (yieldToCollateralSwapper) + FlowYieldVaultsStrategiesV2._setDebtToCollateralSwapper( + uniqueID.id, + SwapConnectors.SequentialSwapper( + swappers: [debtToYieldSwapper, yieldToCollateralSwapper], + uniqueID: uniqueID + ) + ) - // @TODO implement moet to collateral swapper - // let moetToCollateralSwapperKey = FlowYieldVaultsStrategiesV2.getMoetToCollateralSwapperConfigKey(uniqueID) - // - // FlowYieldVaultsStrategiesV2.config[moetToCollateralSwapperKey] = moetToCollateralSwapper - // - switch type { - case Type<@FUSDEVStrategy>(): return <-create FUSDEVStrategy( id: uniqueID, collateralType: collateralType, position: <-position ) + default: panic("Unsupported strategy type \(type.identifier)") } @@ -498,56 +1249,7 @@ access(all) contract FlowYieldVaultsStrategiesV2 { ) return strategyConfig[collateralType] - ?? panic( - "Could not find config for collateral \(collateralType.identifier) when creating Strategy \(strategyType.identifier)" - ) - } - - access(self) fun _resolveTokenBundle( - collateralConfig: FlowYieldVaultsStrategiesV2.CollateralConfig - ): FlowYieldVaultsStrategiesV2.TokenBundle { - // MOET - let moetTokenType = Type<@MOET.Vault>() - let moetTokenEVMAddress = FlowEVMBridgeConfig.getEVMAddressAssociated(with: moetTokenType) - ?? panic("Token Vault type \(moetTokenType.identifier) has not yet been registered with the VMbridge") - - // YIELD (ERC4626 vault token) - let yieldTokenEVMAddress = collateralConfig.yieldTokenEVMAddress - let yieldTokenType = FlowEVMBridgeConfig.getTypeAssociated(with: yieldTokenEVMAddress) - ?? panic( - "Could not retrieve the VM Bridge associated Type for the yield token address \(yieldTokenEVMAddress.toString())" - ) - - // UNDERLYING asset of the ERC4626 vault - let underlying4626AssetEVMAddress = ERC4626Utils.underlyingAssetEVMAddress(vault: yieldTokenEVMAddress) - ?? panic( - "Could not get the underlying asset's EVM address for ERC4626Vault \(yieldTokenEVMAddress.toString())" - ) - let underlying4626AssetType = FlowEVMBridgeConfig.getTypeAssociated(with: underlying4626AssetEVMAddress) - ?? panic( - "Could not retrieve the VM Bridge associated Type for the ERC4626 underlying asset \(underlying4626AssetEVMAddress.toString())" - ) - - return FlowYieldVaultsStrategiesV2.TokenBundle( - moetTokenType: moetTokenType, - moetTokenEVMAddress: moetTokenEVMAddress, - yieldTokenType: yieldTokenType, - yieldTokenEVMAddress: yieldTokenEVMAddress, - underlying4626AssetType: underlying4626AssetType, - underlying4626AssetEVMAddress: underlying4626AssetEVMAddress - ) - } - - access(self) fun _createYieldTokenOracle( - yieldTokenEVMAddress: EVM.EVMAddress, - underlyingAssetType: Type, - uniqueID: DeFiActions.UniqueIdentifier - ): ERC4626PriceOracles.PriceOracle { - return ERC4626PriceOracles.PriceOracle( - vault: yieldTokenEVMAddress, - asset: underlyingAssetType, - uniqueID: uniqueID - ) + ?? panic("Could not find config for collateral \(collateralType.identifier)") } access(self) fun _createUniV3Swapper( @@ -570,13 +1272,12 @@ access(all) contract FlowYieldVaultsStrategiesV2 { ) } - access(self) fun _createMoetToYieldSwapper( - strategyType: Type, + access(self) fun _createDebtToYieldSwapper( tokens: FlowYieldVaultsStrategiesV2.TokenBundle, uniqueID: DeFiActions.UniqueIdentifier ): SwapConnectors.MultiSwapper { // Direct MOET -> YIELD via AMM - let moetToYieldAMM = self._createUniV3Swapper( + let debtToYieldAMM = self._createUniV3Swapper( tokenPath: [tokens.moetTokenEVMAddress, tokens.yieldTokenEVMAddress], feePath: [100], inVault: tokens.moetTokenType, @@ -585,7 +1286,7 @@ access(all) contract FlowYieldVaultsStrategiesV2 { ) // MOET -> UNDERLYING via AMM - let moetToUnderlying = self._createUniV3Swapper( + let debtToUnderlying = self._createUniV3Swapper( tokenPath: [tokens.moetTokenEVMAddress, tokens.underlying4626AssetEVMAddress], feePath: [100], inVault: tokens.moetTokenType, @@ -593,47 +1294,34 @@ access(all) contract FlowYieldVaultsStrategiesV2 { uniqueID: uniqueID ) - // UNDERLYING -> YIELD via ERC4626 vault - // Morpho vaults use MorphoERC4626SwapConnectors; standard ERC4626 vaults use ERC4626SwapConnectors - var underlyingTo4626: {DeFiActions.Swapper}? = nil - if strategyType == Type<@FUSDEVStrategy>() { - underlyingTo4626 = MorphoERC4626SwapConnectors.Swapper( - vaultEVMAddress: tokens.yieldTokenEVMAddress, - coa: FlowYieldVaultsStrategiesV2._getCOACapability(), - feeSource: FlowYieldVaultsStrategiesV2._createFeeSource(withID: uniqueID), - uniqueID: uniqueID, - isReversed: false - ) - } else { - underlyingTo4626 = ERC4626SwapConnectors.Swapper( - asset: tokens.underlying4626AssetType, - vault: tokens.yieldTokenEVMAddress, - coa: FlowYieldVaultsStrategiesV2._getCOACapability(), - feeSource: FlowYieldVaultsStrategiesV2._createFeeSource(withID: uniqueID), - uniqueID: uniqueID - ) - } + // UNDERLYING -> YIELD via Morpho ERC4626 vault deposit + let underlyingTo4626 = MorphoERC4626SwapConnectors.Swapper( + vaultEVMAddress: tokens.yieldTokenEVMAddress, + coa: FlowYieldVaultsStrategiesV2._getCOACapability(), + feeSource: FlowYieldVaultsStrategiesV2._createFeeSource(withID: uniqueID), + uniqueID: uniqueID, + isReversed: false + ) let seq = SwapConnectors.SequentialSwapper( - swappers: [moetToUnderlying, underlyingTo4626!], + swappers: [debtToUnderlying, underlyingTo4626], uniqueID: uniqueID ) return SwapConnectors.MultiSwapper( inVault: tokens.moetTokenType, outVault: tokens.yieldTokenType, - swappers: [moetToYieldAMM, seq], + swappers: [debtToYieldAMM, seq], uniqueID: uniqueID ) } - access(self) fun _createYieldToMoetSwapper( - strategyType: Type, + access(self) fun _createYieldToDebtSwapper( tokens: FlowYieldVaultsStrategiesV2.TokenBundle, uniqueID: DeFiActions.UniqueIdentifier ): SwapConnectors.MultiSwapper { // Direct YIELD -> MOET via AMM - let yieldToMoetAMM = self._createUniV3Swapper( + let yieldToDebtAMM = self._createUniV3Swapper( tokenPath: [tokens.yieldTokenEVMAddress, tokens.moetTokenEVMAddress], feePath: [100], inVault: tokens.yieldTokenType, @@ -641,108 +1329,34 @@ access(all) contract FlowYieldVaultsStrategiesV2 { uniqueID: uniqueID ) - // Reverse path: Morpho vaults support direct redeem; standard ERC4626 vaults use AMM-only path - if strategyType == Type<@FUSDEVStrategy>() { - // YIELD -> UNDERLYING redeem via MorphoERC4626 vault - let yieldToUnderlying = MorphoERC4626SwapConnectors.Swapper( - vaultEVMAddress: tokens.yieldTokenEVMAddress, - coa: FlowYieldVaultsStrategiesV2._getCOACapability(), - feeSource: FlowYieldVaultsStrategiesV2._createFeeSource(withID: uniqueID), - uniqueID: uniqueID, - isReversed: true - ) - // UNDERLYING -> MOET via AMM - let underlyingToMoet = self._createUniV3Swapper( - tokenPath: [tokens.underlying4626AssetEVMAddress, tokens.moetTokenEVMAddress], - feePath: [100], - inVault: tokens.underlying4626AssetType, - outVault: tokens.moetTokenType, - uniqueID: uniqueID - ) - - let seq = SwapConnectors.SequentialSwapper( - swappers: [yieldToUnderlying, underlyingToMoet], - uniqueID: uniqueID - ) - - return SwapConnectors.MultiSwapper( - inVault: tokens.yieldTokenType, - outVault: tokens.moetTokenType, - swappers: [yieldToMoetAMM, seq], - uniqueID: uniqueID - ) - } else { - // Standard ERC4626: AMM-only reverse (no synchronous redeem support) - return SwapConnectors.MultiSwapper( - inVault: tokens.yieldTokenType, - outVault: tokens.moetTokenType, - swappers: [yieldToMoetAMM], - uniqueID: uniqueID - ) - } - } - - /// @TODO - /// implement moet to collateral swapper - // access(self) fun _createMoetToCollateralSwapper( - // strategyType: Type, - // tokens: FlowYieldVaultsStrategiesV2.TokenBundle, - // uniqueID: DeFiActions.UniqueIdentifier - // ): SwapConnectors.MultiSwapper { - // // Direct MOET -> underlying via AMM - // } - - access(self) fun _initAutoBalancerAndIO( - oracle: {DeFiActions.PriceOracle}, - yieldTokenType: Type, - recurringConfig: DeFiActions.AutoBalancerRecurringConfig?, - uniqueID: DeFiActions.UniqueIdentifier - ): FlowYieldVaultsStrategiesV2.AutoBalancerIO { - // NOTE: This stores the AutoBalancer in FlowYieldVaultsAutoBalancers storage and returns an authorized ref. - let autoBalancerRef = - FlowYieldVaultsAutoBalancers._initNewAutoBalancer( - oracle: oracle, - vaultType: yieldTokenType, - lowerThreshold: 0.95, - upperThreshold: 1.05, - rebalanceSink: nil, - rebalanceSource: nil, - recurringConfig: recurringConfig, - uniqueID: uniqueID - ) - - let sink = autoBalancerRef.createBalancerSink() - ?? panic("Could not retrieve Sink from AutoBalancer with id \(uniqueID.id)") - let source = autoBalancerRef.createBalancerSource() - ?? panic("Could not retrieve Source from AutoBalancer with id \(uniqueID.id)") - - return FlowYieldVaultsStrategiesV2.AutoBalancerIO( - autoBalancer: autoBalancerRef, - sink: sink, - source: source + // YIELD -> UNDERLYING redeem via MorphoERC4626 vault + let yieldToUnderlying = MorphoERC4626SwapConnectors.Swapper( + vaultEVMAddress: tokens.yieldTokenEVMAddress, + coa: FlowYieldVaultsStrategiesV2._getCOACapability(), + feeSource: FlowYieldVaultsStrategiesV2._createFeeSource(withID: uniqueID), + uniqueID: uniqueID, + isReversed: true + ) + // UNDERLYING -> MOET via AMM + let underlyingToDebt = self._createUniV3Swapper( + tokenPath: [tokens.underlying4626AssetEVMAddress, tokens.moetTokenEVMAddress], + feePath: [100], + inVault: tokens.underlying4626AssetType, + outVault: tokens.moetTokenType, + uniqueID: uniqueID ) - } - - access(self) fun _openCreditPosition( - funds: @{FungibleToken.Vault}, - issuanceSink: {DeFiActions.Sink}, - repaymentSource: {DeFiActions.Source} - ): @FlowALPv0.Position { - let poolCap = FlowYieldVaultsStrategiesV2.account.storage.copy< - Capability - >(from: FlowALPv0.PoolCapStoragePath) - ?? panic("Missing or invalid pool capability") - - let poolRef = poolCap.borrow() ?? panic("Invalid Pool Cap") - let position <- poolRef.createPosition( - funds: <-funds, - issuanceSink: issuanceSink, - repaymentSource: repaymentSource, - pushToDrawDownSink: true + let seq = SwapConnectors.SequentialSwapper( + swappers: [yieldToUnderlying, underlyingToDebt], + uniqueID: uniqueID ) - return <-position + return SwapConnectors.MultiSwapper( + inVault: tokens.yieldTokenType, + outVault: tokens.moetTokenType, + swappers: [yieldToDebtAMM, seq], + uniqueID: uniqueID + ) } access(self) fun _createYieldToCollateralSwapper( @@ -807,6 +1421,323 @@ access(all) contract FlowYieldVaultsStrategiesV2 { uniqueID: uniqueID ) } + + /// Creates a Collateral → Debt (MOET) swapper using UniswapV3. + /// Path: collateral → underlying (PYUSD0) → MOET + /// + /// The fee for collateral→underlying is the last fee in yieldToCollateral (reversed), + /// and the fee for underlying→MOET is fixed at 100 (0.01%, matching yieldToDebtSwapper). + /// Stored and used by FUSDEVStrategy.closePosition to pre-reduce position debt from + /// collateral when yield tokens alone cannot cover the full outstanding MOET debt. + /// + access(self) fun _createCollateralToDebtSwapper( + collateralConfig: FlowYieldVaultsStrategiesV2.CollateralConfig, + tokens: FlowYieldVaultsStrategiesV2.TokenBundle, + collateralType: Type, + uniqueID: DeFiActions.UniqueIdentifier + ): UniswapV3SwapConnectors.Swapper { + let yieldToCollPath = collateralConfig.yieldToCollateralUniV3AddressPath + let yieldToCollFees = collateralConfig.yieldToCollateralUniV3FeePath + + // collateral EVM address = last element of yieldToCollateral path + // underlying (PYUSD0) EVM address = second element of yieldToCollateral path + assert(yieldToCollPath.length >= 2, message: "yieldToCollateral path must have at least 2 elements") + let collateralEVMAddress = yieldToCollPath[yieldToCollPath.length - 1] + let underlyingEVMAddress = tokens.underlying4626AssetEVMAddress + + // fee[0] = collateral→underlying = last fee in yieldToCollateral (reversed) + // fee[1] = underlying→MOET = 100 (0.01%, matching _createYieldToDebtSwapper) + let collateralToUnderlyingFee = yieldToCollFees[yieldToCollFees.length - 1] + let underlyingToDebtFee: UInt32 = 100 + + return self._createUniV3Swapper( + tokenPath: [collateralEVMAddress, underlyingEVMAddress, tokens.moetTokenEVMAddress], + feePath: [collateralToUnderlyingFee, underlyingToDebtFee], + inVault: collateralType, + outVault: tokens.moetTokenType, + uniqueID: uniqueID + ) + } + } + + /// This StrategyComposer builds strategies that borrow the ERC4626 vault's own underlying + /// asset as debt (e.g. FLOW for syWFLOWv), depositing it directly via ERC4626 deposit/redeem + /// with no AMM swaps. FLOW (the underlying) cannot be used as collateral. + access(all) resource MoreERC4626StrategyComposer : FlowYieldVaults.StrategyComposer { + /// { Strategy Type: { Collateral Type: MoreERC4626CollateralConfig } } + access(self) let config: {Type: {Type: MoreERC4626CollateralConfig}} + + init(_ config: {Type: {Type: MoreERC4626CollateralConfig}}) { + self.config = config + } + + access(all) view fun getComposedStrategyTypes(): {Type: Bool} { + let composed: {Type: Bool} = {} + for t in self.config.keys { + composed[t] = true + } + return composed + } + + access(all) view fun getSupportedInitializationVaults(forStrategy: Type): {Type: Bool} { + let supported: {Type: Bool} = {} + if let strategyConfig = self.config[forStrategy] { + for collateralType in strategyConfig.keys { + supported[collateralType] = true + } + } + return supported + } + + access(self) view fun _supportsCollateral(forStrategy: Type, collateral: Type): Bool { + if let strategyConfig = self.config[forStrategy] { + return strategyConfig[collateral] != nil + } + return false + } + + access(all) view fun getSupportedInstanceVaults(forStrategy: Type, initializedWith: Type): {Type: Bool} { + return self._supportsCollateral(forStrategy: forStrategy, collateral: initializedWith) + ? { initializedWith: true } + : {} + } + + access(all) fun createStrategy( + _ type: Type, + uniqueID: DeFiActions.UniqueIdentifier, + withFunds: @{FungibleToken.Vault} + ): @{FlowYieldVaults.Strategy} { + pre { + self.config[type] != nil: "Unsupported strategy type \(type.identifier)" + } + + switch type { + case Type<@syWFLOWvStrategy>(): + let collateralType = withFunds.getType() + + let stratConfig = self.config[Type<@syWFLOWvStrategy>()] + ?? panic("Could not find config for strategy syWFLOWvStrategy") + let collateralConfig = stratConfig[collateralType] + ?? panic("Could not find config for collateral \(collateralType.identifier) when creating syWFLOWvStrategy") + + let tokens = FlowYieldVaultsStrategiesV2._resolveTokenBundle( + yieldTokenEVMAddress: collateralConfig.yieldTokenEVMAddress + ) + + // Reject FLOW as collateral — it is the vault's underlying / debt token + assert( + collateralType != tokens.underlying4626AssetType, + message: "syWFLOWvStrategy: FLOW cannot be used as collateral — it is the vault's underlying asset" + ) + + let yieldTokenOracle = FlowYieldVaultsStrategiesV2._createYieldTokenOracle( + yieldTokenEVMAddress: tokens.yieldTokenEVMAddress, + underlyingAssetType: tokens.underlying4626AssetType, + uniqueID: uniqueID + ) + + let recurringConfig = FlowYieldVaultsStrategiesV2._createRecurringConfig(withID: uniqueID) + + let balancerIO = FlowYieldVaultsStrategiesV2._initAutoBalancerAndIO( + oracle: yieldTokenOracle, + yieldTokenType: tokens.yieldTokenType, + recurringConfig: recurringConfig, + uniqueID: uniqueID + ) + + // For syWFLOWvStrategy the debt token IS the underlying asset (FLOW), not MOET. + let flowDebtTokenType = tokens.underlying4626AssetType + + // FLOW → syWFLOWv: standard ERC4626 deposit (More vault, not Morpho — no AMM needed) + let flowToSyWFLOWv = ERC4626SwapConnectors.Swapper( + asset: tokens.underlying4626AssetType, + vault: tokens.yieldTokenEVMAddress, + coa: FlowYieldVaultsStrategiesV2._getCOACapability(), + feeSource: FlowYieldVaultsStrategiesV2._createFeeSource(withID: uniqueID), + uniqueID: uniqueID + ) + // syWFLOWv → FLOW: UniV3 AMM swap (More vault does not support synchronous ERC4626 redeem) + let syWFLOWvToFlow = UniswapV3SwapConnectors.Swapper( + factoryAddress: FlowYieldVaultsStrategiesV2.univ3FactoryEVMAddress, + routerAddress: FlowYieldVaultsStrategiesV2.univ3RouterEVMAddress, + quoterAddress: FlowYieldVaultsStrategiesV2.univ3QuoterEVMAddress, + tokenPath: collateralConfig.yieldToUnderlyingUniV3AddressPath, + feePath: collateralConfig.yieldToUnderlyingUniV3FeePath, + inVault: tokens.yieldTokenType, + outVault: tokens.underlying4626AssetType, + coaCapability: FlowYieldVaultsStrategiesV2._getCOACapability(), + uniqueID: uniqueID + ) + + // issuanceSink: pool pushes borrowed FLOW → deposit → syWFLOWv → AutoBalancer + let abaSwapSinkFlow = SwapConnectors.SwapSink( + swapper: flowToSyWFLOWv, + sink: balancerIO.sink, + uniqueID: uniqueID + ) + // repaymentSource: AutoBalancer → syWFLOWv → AMM swap → FLOW → pool + let abaSwapSourceFlow = SwapConnectors.SwapSource( + swapper: syWFLOWvToFlow, + source: balancerIO.source, + uniqueID: uniqueID + ) + + // --- Stablecoin pre-swap path (e.g. PYUSD0 → MOET) --- + if let preswapCfg = FlowYieldVaultsStrategiesV2._getMoetPreswapConfig( + composer: Type<@MoreERC4626StrategyComposer>(), + collateral: collateralType + ) { + let preSwapper = UniswapV3SwapConnectors.Swapper( + factoryAddress: FlowYieldVaultsStrategiesV2.univ3FactoryEVMAddress, + routerAddress: FlowYieldVaultsStrategiesV2.univ3RouterEVMAddress, + quoterAddress: FlowYieldVaultsStrategiesV2.univ3QuoterEVMAddress, + tokenPath: preswapCfg.collateralToMoetAddressPath, + feePath: preswapCfg.collateralToMoetFeePath, + inVault: collateralType, + outVault: tokens.moetTokenType, + coaCapability: FlowYieldVaultsStrategiesV2._getCOACapability(), + uniqueID: uniqueID + ) + let preSwapQuote = preSwapper.quoteOut(forProvided: withFunds.balance, reverse: false) + let moetFunds <- preSwapper.swap(quote: preSwapQuote, inVault: <-withFunds) + + // Open FlowALP position with MOET as collateral + let positionPreswap <- FlowYieldVaultsStrategiesV2._openCreditPosition( + funds: <-moetFunds, + issuanceSink: abaSwapSinkFlow, + repaymentSource: abaSwapSourceFlow + ) + + // AutoBalancer debt management: same as WBTC/WETH (manages FLOW borrow/repay) + let positionDebtSinkPre = positionPreswap.createSinkWithOptions( + type: flowDebtTokenType, + pushToDrawDownSink: false + ) + let positionDebtSwapSinkPre = SwapConnectors.SwapSink( + swapper: syWFLOWvToFlow, + sink: positionDebtSinkPre, + uniqueID: uniqueID + ) + let positionDebtSourcePre = positionPreswap.createSourceWithOptions( + type: flowDebtTokenType, + pullFromTopUpSource: false + ) + let positionDebtSwapSourcePre = SwapConnectors.SwapSource( + swapper: flowToSyWFLOWv, + source: positionDebtSourcePre, + uniqueID: uniqueID + ) + balancerIO.autoBalancer.setSink(positionDebtSwapSinkPre, updateSinkID: true) + balancerIO.autoBalancer.setSource(positionDebtSwapSourcePre, updateSourceID: true) + + // FLOW→MOET: converts FLOW dust back to MOET (internal collateral) in closePosition. + // Path: FLOW → collateral (debtToCollateral) → MOET (pre-swap reversed hop). + // e.g. for PYUSD0: WFLOW→(fee 500)→PYUSD0→(fee 100)→MOET (2-hop). + var flowToMoetPath = collateralConfig.debtToCollateralUniV3AddressPath + flowToMoetPath.append(preswapCfg.collateralToMoetAddressPath[preswapCfg.collateralToMoetAddressPath.length - 1]) + var flowToMoetFees = collateralConfig.debtToCollateralUniV3FeePath + flowToMoetFees.append(preswapCfg.collateralToMoetFeePath[preswapCfg.collateralToMoetFeePath.length - 1]) + let flowToMoet = UniswapV3SwapConnectors.Swapper( + factoryAddress: FlowYieldVaultsStrategiesV2.univ3FactoryEVMAddress, + routerAddress: FlowYieldVaultsStrategiesV2.univ3RouterEVMAddress, + quoterAddress: FlowYieldVaultsStrategiesV2.univ3QuoterEVMAddress, + tokenPath: flowToMoetPath, + feePath: flowToMoetFees, + inVault: tokens.underlying4626AssetType, // FLOW + outVault: tokens.moetTokenType, // MOET + coaCapability: FlowYieldVaultsStrategiesV2._getCOACapability(), + uniqueID: uniqueID + ) + + // MOET→collateral (e.g. PYUSD0): final conversion in closePosition + let moetToCollateral = UniswapV3SwapConnectors.Swapper( + factoryAddress: FlowYieldVaultsStrategiesV2.univ3FactoryEVMAddress, + routerAddress: FlowYieldVaultsStrategiesV2.univ3RouterEVMAddress, + quoterAddress: FlowYieldVaultsStrategiesV2.univ3QuoterEVMAddress, + tokenPath: preswapCfg.collateralToMoetAddressPath.reverse(), + feePath: preswapCfg.collateralToMoetFeePath.reverse(), + inVault: tokens.moetTokenType, + outVault: collateralType, + coaCapability: FlowYieldVaultsStrategiesV2._getCOACapability(), + uniqueID: uniqueID + ) + + FlowYieldVaultsStrategiesV2._setSyWFLOWvDebtTokenType(uniqueID.id, flowDebtTokenType) + FlowYieldVaultsStrategiesV2._setOriginalCollateralType(uniqueID.id, collateralType) + FlowYieldVaultsStrategiesV2._setCollateralPreSwapper(uniqueID.id, preSwapper) + FlowYieldVaultsStrategiesV2._setMoetToCollateralSwapper(uniqueID.id, moetToCollateral) + + return <-create syWFLOWvStrategy( + id: uniqueID, + collateralType: tokens.moetTokenType, + position: <-positionPreswap, + yieldToDebtSwapper: syWFLOWvToFlow, + debtToCollateralSwapper: flowToMoet + ) + } + + // --- Standard path (WBTC, WETH — directly supported by FlowALP) --- + + // Open FlowALP position with collateral; drawDownSink accepts FLOW + let positionFlow <- FlowYieldVaultsStrategiesV2._openCreditPosition( + funds: <-withFunds, + issuanceSink: abaSwapSinkFlow, + repaymentSource: abaSwapSourceFlow + ) + + // AutoBalancer overflow: excess syWFLOWv → AMM swap → FLOW → repay position debt + let positionDebtSink = positionFlow.createSinkWithOptions( + type: flowDebtTokenType, + pushToDrawDownSink: false + ) + let positionDebtSwapSink = SwapConnectors.SwapSink( + swapper: syWFLOWvToFlow, + sink: positionDebtSink, + uniqueID: uniqueID + ) + + // AutoBalancer deficit: borrow more FLOW from position → deposit → syWFLOWv + let positionDebtSource = positionFlow.createSourceWithOptions( + type: flowDebtTokenType, + pullFromTopUpSource: false + ) + let positionDebtSwapSource = SwapConnectors.SwapSource( + swapper: flowToSyWFLOWv, + source: positionDebtSource, + uniqueID: uniqueID + ) + + balancerIO.autoBalancer.setSink(positionDebtSwapSink, updateSinkID: true) + balancerIO.autoBalancer.setSource(positionDebtSwapSource, updateSourceID: true) + + // FLOW→collateral swapper for dust conversion in closePosition + let flowToCollateral = UniswapV3SwapConnectors.Swapper( + factoryAddress: FlowYieldVaultsStrategiesV2.univ3FactoryEVMAddress, + routerAddress: FlowYieldVaultsStrategiesV2.univ3RouterEVMAddress, + quoterAddress: FlowYieldVaultsStrategiesV2.univ3QuoterEVMAddress, + tokenPath: collateralConfig.debtToCollateralUniV3AddressPath, + feePath: collateralConfig.debtToCollateralUniV3FeePath, + inVault: tokens.underlying4626AssetType, + outVault: collateralType, + coaCapability: FlowYieldVaultsStrategiesV2._getCOACapability(), + uniqueID: uniqueID + ) + + // Store debtTokenType in contract-level config (resource field removed for upgrade compat) + FlowYieldVaultsStrategiesV2._setSyWFLOWvDebtTokenType(uniqueID.id, flowDebtTokenType) + + return <-create syWFLOWvStrategy( + id: uniqueID, + collateralType: collateralType, + position: <-positionFlow, + yieldToDebtSwapper: syWFLOWvToFlow, + debtToCollateralSwapper: flowToCollateral + ) + + default: + panic("Unsupported strategy type \(type.identifier)") + } + } } access(all) entitlement Configure @@ -832,14 +1763,45 @@ access(all) contract FlowYieldVaultsStrategiesV2 { yieldToCollateralUniV3FeePath: yieldToCollateralFeePath ) } + + access(self) + fun makeMoreERC4626CollateralConfig( + yieldTokenEVMAddress: EVM.EVMAddress, + yieldToUnderlyingAddressPath: [EVM.EVMAddress], + yieldToUnderlyingFeePath: [UInt32], + debtToCollateralAddressPath: [EVM.EVMAddress], + debtToCollateralFeePath: [UInt32] + ): MoreERC4626CollateralConfig { + pre { + yieldToUnderlyingAddressPath.length > 1: + "Invalid Uniswap V3 swap path length" + yieldToUnderlyingFeePath.length == yieldToUnderlyingAddressPath.length - 1: + "Uniswap V3 fee path length must be path length - 1" + yieldToUnderlyingAddressPath[0].equals(yieldTokenEVMAddress): + "UniswapV3 swap path must start with yield token" + debtToCollateralAddressPath.length > 1: + "Invalid debt-to-collateral Uniswap V3 path length" + debtToCollateralFeePath.length == debtToCollateralAddressPath.length - 1: + "Debt-to-collateral Uniswap V3 fee path length must be path length - 1" + } + return MoreERC4626CollateralConfig( + yieldTokenEVMAddress: yieldTokenEVMAddress, + yieldToUnderlyingUniV3AddressPath: yieldToUnderlyingAddressPath, + yieldToUnderlyingUniV3FeePath: yieldToUnderlyingFeePath, + debtToCollateralUniV3AddressPath: debtToCollateralAddressPath, + debtToCollateralUniV3FeePath: debtToCollateralFeePath + ) + } + /// This resource enables the issuance of StrategyComposers, thus safeguarding the issuance of Strategies which /// may utilize resource consumption (i.e. account storage). Since Strategy creation consumes account storage /// via configured AutoBalancers access(all) resource StrategyComposerIssuer : FlowYieldVaults.StrategyComposerIssuer { - /// { StrategyComposer Type: { Strategy Type: { Collateral Type: FlowYieldVaultsStrategiesV2.CollateralConfig } } } - access(all) var configs: {Type: {Type: {Type: FlowYieldVaultsStrategiesV2.CollateralConfig}}} + /// { Composer Type: { Strategy Type: { Collateral Type: CollateralConfig } } } + /// Used by MorphoERC4626StrategyComposer. + access(all) var configs: {Type: {Type: {Type: CollateralConfig}}} - init(configs: {Type: {Type: {Type: FlowYieldVaultsStrategiesV2.CollateralConfig}}}) { + init(configs: {Type: {Type: {Type: CollateralConfig}}}) { self.configs = configs } @@ -848,49 +1810,76 @@ access(all) contract FlowYieldVaultsStrategiesV2 { strategy: Type, collateral: Type ): Bool { - if let composerConfig = self.configs[composer] { - if let strategyConfig = composerConfig[strategy] { - return strategyConfig[collateral] != nil + if let composerPartition = self.configs[composer] { + if let stratPartition = composerPartition[strategy] { + if stratPartition[collateral] != nil { return true } } } - return false + return FlowYieldVaultsStrategiesV2._getMoreERC4626Config( + composer: composer, strategy: strategy, collateral: collateral + ) != nil } access(all) view fun getSupportedComposers(): {Type: Bool} { - return { - Type<@MorphoERC4626StrategyComposer>(): true + return { + Type<@MorphoERC4626StrategyComposer>(): true, + Type<@MoreERC4626StrategyComposer>(): true } } access(self) view fun isSupportedComposer(_ type: Type): Bool { return type == Type<@MorphoERC4626StrategyComposer>() + || type == Type<@MoreERC4626StrategyComposer>() } + access(all) fun issueComposer(_ type: Type): @{FlowYieldVaults.StrategyComposer} { pre { - self.isSupportedComposer(type) == true: - "Unsupported StrategyComposer \(type.identifier) requested" - self.configs[type] != nil: - "Could not find config for StrategyComposer \(type.identifier)" + self.isSupportedComposer(type): "Unsupported StrategyComposer \(type.identifier) requested" } switch type { case Type<@MorphoERC4626StrategyComposer>(): - return <- create MorphoERC4626StrategyComposer(self.configs[type]!) + return <- create MorphoERC4626StrategyComposer(self.configs[type] ?? {}) + case Type<@MoreERC4626StrategyComposer>(): + return <- create MoreERC4626StrategyComposer( + FlowYieldVaultsStrategiesV2._getMoreERC4626ComposerConfig(type) + ) default: panic("Unsupported StrategyComposer \(type.identifier) requested") } } + /// Merges new CollateralConfig entries into the MorphoERC4626StrategyComposer config. access(Configure) - fun upsertConfigFor( - composer: Type, + fun upsertMorphoConfig( config: {Type: {Type: FlowYieldVaultsStrategiesV2.CollateralConfig}} ) { - pre { - self.isSupportedComposer(composer) == true: - "Unsupported StrategyComposer Type \(composer.identifier)" + for stratType in config.keys { + assert(stratType.isSubtype(of: Type<@{FlowYieldVaults.Strategy}>()), + message: "Invalid config key \(stratType.identifier) - not a FlowYieldVaults.Strategy Type") + for collateralType in config[stratType]!.keys { + assert(collateralType.isSubtype(of: Type<@{FungibleToken.Vault}>()), + message: "Invalid config key at config[\(stratType.identifier)] - \(collateralType.identifier) is not a FungibleToken.Vault") + } } - // Validate keys + let composerType = Type<@MorphoERC4626StrategyComposer>() + var composerPartition = self.configs[composerType] ?? {} + for stratType in config.keys { + var stratPartition: {Type: CollateralConfig} = composerPartition[stratType] ?? {} + let newPerCollateral = config[stratType]! + for collateralType in newPerCollateral.keys { + stratPartition[collateralType] = newPerCollateral[collateralType]! + } + composerPartition[stratType] = stratPartition + } + self.configs[composerType] = composerPartition + } + + /// Merges new MoreERC4626CollateralConfig entries into the MoreERC4626StrategyComposer config. + access(Configure) + fun upsertMoreERC4626Config( + config: {Type: {Type: FlowYieldVaultsStrategiesV2.MoreERC4626CollateralConfig}} + ) { for stratType in config.keys { assert(stratType.isSubtype(of: Type<@{FlowYieldVaults.Strategy}>()), message: "Invalid config key \(stratType.identifier) - not a FlowYieldVaults.Strategy Type") @@ -900,26 +1889,21 @@ access(all) contract FlowYieldVaultsStrategiesV2 { } } - // Merge instead of overwrite - let existingComposerConfig = self.configs[composer] ?? {} - var mergedComposerConfig = existingComposerConfig - + let composerType = Type<@MoreERC4626StrategyComposer>() for stratType in config.keys { let newPerCollateral = config[stratType]! - let existingPerCollateral = mergedComposerConfig[stratType] ?? {} - var mergedPerCollateral: {Type: FlowYieldVaultsStrategiesV2.CollateralConfig} = existingPerCollateral - for collateralType in newPerCollateral.keys { - mergedPerCollateral[collateralType] = newPerCollateral[collateralType]! + FlowYieldVaultsStrategiesV2._setMoreERC4626Config( + composer: composerType, + strategy: stratType, + collateral: collateralType, + cfg: newPerCollateral[collateralType]! + ) } - mergedComposerConfig[stratType] = mergedPerCollateral } - - self.configs[composer] = mergedComposerConfig } - access(Configure) fun addOrUpdateCollateralConfig( - composer: Type, + access(Configure) fun addOrUpdateMorphoCollateralConfig( strategyType: Type, collateralVaultType: Type, yieldTokenEVMAddress: EVM.EVMAddress, @@ -927,36 +1911,87 @@ access(all) contract FlowYieldVaultsStrategiesV2 { yieldToCollateralFeePath: [UInt32] ) { pre { - self.isSupportedComposer(composer) == true: - "Unsupported StrategyComposer Type \(composer.identifier)" strategyType.isSubtype(of: Type<@{FlowYieldVaults.Strategy}>()): "Strategy type \(strategyType.identifier) is not a FlowYieldVaults.Strategy" collateralVaultType.isSubtype(of: Type<@{FungibleToken.Vault}>()): "Collateral type \(collateralVaultType.identifier) is not a FungibleToken.Vault" } - // Base struct with shared addresses - var base = FlowYieldVaultsStrategiesV2.makeCollateralConfig( + let base = FlowYieldVaultsStrategiesV2.makeCollateralConfig( yieldTokenEVMAddress: yieldTokenEVMAddress, yieldToCollateralAddressPath: yieldToCollateralAddressPath, yieldToCollateralFeePath: yieldToCollateralFeePath ) + self.upsertMorphoConfig(config: { strategyType: { collateralVaultType: base } }) + } - // Wrap into the nested config expected by upsertConfigFor - let singleCollateralConfig = { - strategyType: { - collateralVaultType: base - } + access(Configure) fun addOrUpdateMoreERC4626CollateralConfig( + strategyType: Type, + collateralVaultType: Type, + yieldTokenEVMAddress: EVM.EVMAddress, + yieldToUnderlyingAddressPath: [EVM.EVMAddress], + yieldToUnderlyingFeePath: [UInt32], + debtToCollateralAddressPath: [EVM.EVMAddress], + debtToCollateralFeePath: [UInt32] + ) { + pre { + strategyType.isSubtype(of: Type<@{FlowYieldVaults.Strategy}>()): + "Strategy type \(strategyType.identifier) is not a FlowYieldVaults.Strategy" + collateralVaultType.isSubtype(of: Type<@{FungibleToken.Vault}>()): + "Collateral type \(collateralVaultType.identifier) is not a FungibleToken.Vault" } - self.upsertConfigFor(composer: composer, config: singleCollateralConfig) + let cfg = FlowYieldVaultsStrategiesV2.makeMoreERC4626CollateralConfig( + yieldTokenEVMAddress: yieldTokenEVMAddress, + yieldToUnderlyingAddressPath: yieldToUnderlyingAddressPath, + yieldToUnderlyingFeePath: yieldToUnderlyingFeePath, + debtToCollateralAddressPath: debtToCollateralAddressPath, + debtToCollateralFeePath: debtToCollateralFeePath + ) + self.upsertMoreERC4626Config(config: { strategyType: { collateralVaultType: cfg } }) } + + /// Configures a stablecoin collateral type to use MOET pre-swap before depositing into + /// FlowALP. Required for stablecoins like PYUSD0 that FlowALP does not support directly + /// as collateral (it only accepts MOET as its stablecoin). + /// + /// collateralToMoetAddressPath: full UniV3 path from collateral EVM address → MOET EVM address + /// collateralToMoetFeePath: UniV3 fee tiers for each hop (length = path length - 1) + access(Configure) fun upsertMoetPreswapConfig( + composer: Type, + collateralVaultType: Type, + collateralToMoetAddressPath: [EVM.EVMAddress], + collateralToMoetFeePath: [UInt32] + ) { + pre { + composer == Type<@MorphoERC4626StrategyComposer>() + || composer == Type<@MoreERC4626StrategyComposer>(): + "composer must be MorphoERC4626StrategyComposer or MoreERC4626StrategyComposer" + collateralVaultType.isSubtype(of: Type<@{FungibleToken.Vault}>()): + "collateralVaultType must be a FungibleToken.Vault" + collateralToMoetAddressPath.length > 1: + "Path must have at least 2 elements" + collateralToMoetFeePath.length == collateralToMoetAddressPath.length - 1: + "Fee path length must equal address path length - 1" + } + FlowYieldVaultsStrategiesV2._setMoetPreswapConfig( + composer: composer, + collateral: collateralVaultType, + cfg: FlowYieldVaultsStrategiesV2.MoetPreswapConfig( + collateralToMoetAddressPath: collateralToMoetAddressPath, + collateralToMoetFeePath: collateralToMoetFeePath + ) + ) + } + access(Configure) fun purgeConfig() { self.configs = { Type<@MorphoERC4626StrategyComposer>(): { - Type<@FUSDEVStrategy>(): {} as {Type: FlowYieldVaultsStrategiesV2.CollateralConfig} + Type<@FUSDEVStrategy>(): {} as {Type: CollateralConfig} } } + FlowYieldVaultsStrategiesV2._purgeMoreERC4626Configs() + FlowYieldVaultsStrategiesV2.config["moetPreswapConfigs"] = {} as {Type: {Type: FlowYieldVaultsStrategiesV2.MoetPreswapConfig}} } } @@ -994,7 +2029,7 @@ access(all) contract FlowYieldVaultsStrategiesV2 { fun _createRecurringConfig(withID: DeFiActions.UniqueIdentifier?): DeFiActions.AutoBalancerRecurringConfig { // Create txnFunder that can provide/accept FLOW for scheduling fees let txnFunder = self._createTxnFunder(withID: withID) - + return DeFiActions.AutoBalancerRecurringConfig( interval: 60 * 10, // Rebalance every 10 minutes priority: FlowTransactionScheduler.Priority.Medium, @@ -1022,18 +2057,238 @@ access(all) contract FlowYieldVaultsStrategiesV2 { ) } - access(self) view fun getYieldToMoetSwapperConfigKey(_ uniqueID: DeFiActions.UniqueIdentifier?): String { - pre { - uniqueID != nil: "Missing UniqueIdentifier for swapper config key" + // --- "yieldToMoetSwappers" partition --- + + access(contract) view fun _getYieldToMoetSwapper(_ id: UInt64): {DeFiActions.Swapper}? { + let partition = FlowYieldVaultsStrategiesV2.config["yieldToMoetSwappers"] as! {UInt64: {DeFiActions.Swapper}}? ?? {} + return partition[id] + } + + access(contract) fun _setYieldToMoetSwapper(_ id: UInt64, _ swapper: {DeFiActions.Swapper}) { + var partition = FlowYieldVaultsStrategiesV2.config["yieldToMoetSwappers"] as! {UInt64: {DeFiActions.Swapper}}? ?? {} + partition[id] = swapper + FlowYieldVaultsStrategiesV2.config["yieldToMoetSwappers"] = partition + } + + // --- "debtToCollateralSwappers" partition --- + + access(contract) view fun _getDebtToCollateralSwapper(_ id: UInt64): {DeFiActions.Swapper}? { + let partition = FlowYieldVaultsStrategiesV2.config["debtToCollateralSwappers"] as! {UInt64: {DeFiActions.Swapper}}? ?? {} + return partition[id] + } + + access(contract) fun _setDebtToCollateralSwapper(_ id: UInt64, _ swapper: {DeFiActions.Swapper}) { + var partition = FlowYieldVaultsStrategiesV2.config["debtToCollateralSwappers"] as! {UInt64: {DeFiActions.Swapper}}? ?? {} + partition[id] = swapper + FlowYieldVaultsStrategiesV2.config["debtToCollateralSwappers"] = partition + } + + access(contract) fun _removeDebtToCollateralSwapper(_ id: UInt64) { + var partition = FlowYieldVaultsStrategiesV2.config["debtToCollateralSwappers"] as! {UInt64: {DeFiActions.Swapper}}? ?? {} + partition.remove(key: id) + FlowYieldVaultsStrategiesV2.config["debtToCollateralSwappers"] = partition + } + + // --- "collateralToDebtSwappers" partition --- + // Stores a collateral→debt (collateral→PYUSD0→MOET) UniV3 swapper per FUSDEVStrategy uniqueID. + // Used in FUSDEVStrategy.closePosition to pre-supplement the debt when yield tokens alone are + // insufficient (e.g. ~0.02% round-trip fee shortfall with no accrued yield). + + access(contract) view fun _getCollateralToDebtSwapper(_ id: UInt64): {DeFiActions.Swapper}? { + let partition = FlowYieldVaultsStrategiesV2.config["collateralToDebtSwappers"] as! {UInt64: {DeFiActions.Swapper}}? ?? {} + return partition[id] + } + + access(contract) fun _setCollateralToDebtSwapper(_ id: UInt64, _ swapper: {DeFiActions.Swapper}) { + var partition = FlowYieldVaultsStrategiesV2.config["collateralToDebtSwappers"] as! {UInt64: {DeFiActions.Swapper}}? ?? {} + partition[id] = swapper + FlowYieldVaultsStrategiesV2.config["collateralToDebtSwappers"] = partition + } + + // --- "closedPositions" partition --- + + access(contract) view fun _isPositionClosed(_ uniqueID: DeFiActions.UniqueIdentifier?): Bool { + if uniqueID == nil { return false } + let partition = FlowYieldVaultsStrategiesV2.config["closedPositions"] as! {UInt64: Bool}? ?? {} + return partition[uniqueID!.id] ?? false + } + + access(contract) fun _markPositionClosed(_ uniqueID: DeFiActions.UniqueIdentifier?) { + if let id = uniqueID { + var partition = FlowYieldVaultsStrategiesV2.config["closedPositions"] as! {UInt64: Bool}? ?? {} + partition[id.id] = true + FlowYieldVaultsStrategiesV2.config["closedPositions"] = partition } - return "yieldToMoetSwapper_\(uniqueID!.id.toString())" } - access(self) view fun getMoetToCollateralSwapperConfigKey(_ uniqueID: DeFiActions.UniqueIdentifier?): String { - pre { - uniqueID != nil: "Missing UniqueIdentifier for swapper config key" + access(contract) fun _cleanupPositionClosed(_ uniqueID: DeFiActions.UniqueIdentifier?) { + if let id = uniqueID { + var partition = FlowYieldVaultsStrategiesV2.config["closedPositions"] as! {UInt64: Bool}? ?? {} + partition.remove(key: id.id) + FlowYieldVaultsStrategiesV2.config["closedPositions"] = partition } - return "moetToCollateralSwapper_\(uniqueID!.id.toString())" + } + + // --- "syWFLOWvDebtTokenTypes" partition --- + // Stores the debt token Type per syWFLOWvStrategy uniqueID. + // Kept in the contract-level config map so no new field is added to the deployed syWFLOWvStrategy resource. + + access(contract) view fun _getSyWFLOWvDebtTokenType(_ id: UInt64): Type? { + let partition = FlowYieldVaultsStrategiesV2.config["syWFLOWvDebtTokenTypes"] as! {UInt64: Type}? ?? {} + return partition[id] + } + + access(contract) fun _setSyWFLOWvDebtTokenType(_ id: UInt64, _ t: Type) { + var partition = FlowYieldVaultsStrategiesV2.config["syWFLOWvDebtTokenTypes"] as! {UInt64: Type}? ?? {} + partition[id] = t + FlowYieldVaultsStrategiesV2.config["syWFLOWvDebtTokenTypes"] = partition + } + + access(contract) fun _removeSyWFLOWvDebtTokenType(_ id: UInt64?) { + if let unwrappedID = id { + var partition = FlowYieldVaultsStrategiesV2.config["syWFLOWvDebtTokenTypes"] as! {UInt64: Type}? ?? {} + partition.remove(key: unwrappedID) + FlowYieldVaultsStrategiesV2.config["syWFLOWvDebtTokenTypes"] = partition + } + } + + // --- "moreERC4626Configs" partition --- + // Stores MoreERC4626CollateralConfig keyed by composer type → strategy type → collateral type. + // Kept in the contract-level config map so no new field is added to the deployed StrategyComposerIssuer resource. + + access(contract) view fun _getMoreERC4626Config( + composer: Type, + strategy: Type, + collateral: Type + ): MoreERC4626CollateralConfig? { + let partition = FlowYieldVaultsStrategiesV2.config["moreERC4626Configs"] + as! {Type: {Type: {Type: MoreERC4626CollateralConfig}}}? ?? {} + if let composerPart = partition[composer] { + if let stratPart = composerPart[strategy] { + return stratPart[collateral] + } + } + return nil + } + + access(contract) view fun _getMoreERC4626ComposerConfig( + _ composerType: Type + ): {Type: {Type: MoreERC4626CollateralConfig}} { + let partition = FlowYieldVaultsStrategiesV2.config["moreERC4626Configs"] + as! {Type: {Type: {Type: MoreERC4626CollateralConfig}}}? ?? {} + return partition[composerType] ?? {} + } + + access(contract) fun _setMoreERC4626Config( + composer: Type, + strategy: Type, + collateral: Type, + cfg: MoreERC4626CollateralConfig + ) { + var partition = FlowYieldVaultsStrategiesV2.config["moreERC4626Configs"] + as! {Type: {Type: {Type: MoreERC4626CollateralConfig}}}? ?? {} + var composerPartition = partition[composer] ?? {} + var stratPartition = composerPartition[strategy] ?? {} + stratPartition[collateral] = cfg + composerPartition[strategy] = stratPartition + partition[composer] = composerPartition + FlowYieldVaultsStrategiesV2.config["moreERC4626Configs"] = partition + } + + access(contract) fun _purgeMoreERC4626Configs() { + FlowYieldVaultsStrategiesV2.config["moreERC4626Configs"] = {} as {Type: {Type: {Type: MoreERC4626CollateralConfig}}} + } + + // --- "originalCollateralTypes" partition --- + // Stores the original (external) collateral type per strategy uniqueID when a MOET pre-swap + // is in effect. E.g. PYUSD0 when the position internally holds MOET. + + access(contract) view fun _getOriginalCollateralType(_ id: UInt64): Type? { + let partition = FlowYieldVaultsStrategiesV2.config["originalCollateralTypes"] as! {UInt64: Type}? ?? {} + return partition[id] + } + + access(contract) fun _setOriginalCollateralType(_ id: UInt64, _ t: Type) { + var partition = FlowYieldVaultsStrategiesV2.config["originalCollateralTypes"] as! {UInt64: Type}? ?? {} + partition[id] = t + FlowYieldVaultsStrategiesV2.config["originalCollateralTypes"] = partition + } + + access(contract) fun _removeOriginalCollateralType(_ id: UInt64) { + var partition = FlowYieldVaultsStrategiesV2.config["originalCollateralTypes"] as! {UInt64: Type}? ?? {} + partition.remove(key: id) + FlowYieldVaultsStrategiesV2.config["originalCollateralTypes"] = partition + } + + // --- "collateralPreSwappers" partition --- + // Stores a collateral→MOET swapper per strategy uniqueID. + // Used in deposit() to pre-swap incoming stablecoin collateral (e.g. PYUSD0) to MOET. + + access(contract) view fun _getCollateralPreSwapper(_ id: UInt64): {DeFiActions.Swapper}? { + let partition = FlowYieldVaultsStrategiesV2.config["collateralPreSwappers"] as! {UInt64: {DeFiActions.Swapper}}? ?? {} + return partition[id] + } + + access(contract) fun _setCollateralPreSwapper(_ id: UInt64, _ swapper: {DeFiActions.Swapper}) { + var partition = FlowYieldVaultsStrategiesV2.config["collateralPreSwappers"] as! {UInt64: {DeFiActions.Swapper}}? ?? {} + partition[id] = swapper + FlowYieldVaultsStrategiesV2.config["collateralPreSwappers"] = partition + } + + access(contract) fun _removeCollateralPreSwapper(_ id: UInt64) { + var partition = FlowYieldVaultsStrategiesV2.config["collateralPreSwappers"] as! {UInt64: {DeFiActions.Swapper}}? ?? {} + partition.remove(key: id) + FlowYieldVaultsStrategiesV2.config["collateralPreSwappers"] = partition + } + + // --- "moetToCollateralSwappers" partition --- + // Stores a MOET→original-collateral swapper per strategy uniqueID (FUSDEVStrategy and + // syWFLOWvStrategy). Built from the reversed MoetPreswapConfig path (same path in reverse). + // Used in closePosition to convert returned MOET collateral back to the original stablecoin + // (e.g. PYUSD0) for the no-debt path. The regular close path uses debtToCollateralSwappers. + + access(contract) view fun _getMoetToCollateralSwapper(_ id: UInt64): {DeFiActions.Swapper}? { + let partition = FlowYieldVaultsStrategiesV2.config["moetToCollateralSwappers"] as! {UInt64: {DeFiActions.Swapper}}? ?? {} + return partition[id] + } + + access(contract) fun _setMoetToCollateralSwapper(_ id: UInt64, _ swapper: {DeFiActions.Swapper}) { + var partition = FlowYieldVaultsStrategiesV2.config["moetToCollateralSwappers"] as! {UInt64: {DeFiActions.Swapper}}? ?? {} + partition[id] = swapper + FlowYieldVaultsStrategiesV2.config["moetToCollateralSwappers"] = partition + } + + access(contract) fun _removeMoetToCollateralSwapper(_ id: UInt64) { + var partition = FlowYieldVaultsStrategiesV2.config["moetToCollateralSwappers"] as! {UInt64: {DeFiActions.Swapper}}? ?? {} + partition.remove(key: id) + FlowYieldVaultsStrategiesV2.config["moetToCollateralSwappers"] = partition + } + + // --- "moetPreswapConfigs" partition --- + // Static admin config: keyed by composerType → collateralType → MoetPreswapConfig. + // Checked during createStrategy to determine whether a collateral needs MOET pre-swap. + + access(contract) view fun _getMoetPreswapConfig( + composer: Type, + collateral: Type + ): MoetPreswapConfig? { + let partition = FlowYieldVaultsStrategiesV2.config["moetPreswapConfigs"] + as! {Type: {Type: MoetPreswapConfig}}? ?? {} + let p = partition[composer] ?? {} + return p[collateral] + } + + access(contract) fun _setMoetPreswapConfig( + composer: Type, + collateral: Type, + cfg: MoetPreswapConfig + ) { + var partition = FlowYieldVaultsStrategiesV2.config["moetPreswapConfigs"] + as! {Type: {Type: MoetPreswapConfig}}? ?? {} + var p = partition[composer] ?? {} + p[collateral] = cfg + partition[composer] = p + FlowYieldVaultsStrategiesV2.config["moetPreswapConfigs"] = partition } init( @@ -1052,12 +2307,14 @@ access(all) contract FlowYieldVaultsStrategiesV2 { panic("Could not find EVM address for \(moetType.identifier) - ensure the asset is onboarded to the VM Bridge") } - let configs = { + let issuer <- create StrategyComposerIssuer( + configs: { Type<@MorphoERC4626StrategyComposer>(): { - Type<@FUSDEVStrategy>(): {} as {Type: FlowYieldVaultsStrategiesV2.CollateralConfig} + Type<@FUSDEVStrategy>(): {} as {Type: CollateralConfig} } } - self.account.storage.save(<-create StrategyComposerIssuer(configs: configs), to: self.IssuerStoragePath) + ) + self.account.storage.save(<-issuer, to: self.IssuerStoragePath) // TODO: this is temporary until we have a better way to pass user's COAs to inner connectors // create a COA in this account From 0fc9afc7a17d0ae76ea8c9a40969f08072828630 Mon Sep 17 00:00:00 2001 From: Alex <12097569+nialexsan@users.noreply.github.com> Date: Mon, 16 Mar 2026 11:05:59 -0400 Subject: [PATCH 02/29] remove More and syWFLOWv from strategy --- .../contracts/FlowYieldVaultsStrategiesV2.cdc | 793 +----------------- 1 file changed, 5 insertions(+), 788 deletions(-) diff --git a/cadence/contracts/FlowYieldVaultsStrategiesV2.cdc b/cadence/contracts/FlowYieldVaultsStrategiesV2.cdc index 22e19374..f0fdbfc7 100644 --- a/cadence/contracts/FlowYieldVaultsStrategiesV2.cdc +++ b/cadence/contracts/FlowYieldVaultsStrategiesV2.cdc @@ -49,8 +49,6 @@ access(all) contract FlowYieldVaultsStrategiesV2 { /// "debtToCollateralSwappers" → {UInt64: {DeFiActions.Swapper}} /// "collateralToDebtSwappers" → {UInt64: {DeFiActions.Swapper}} /// "closedPositions" → {UInt64: Bool} - /// "syWFLOWvDebtTokenTypes" → {UInt64: Type} - /// "moreERC4626Configs" → {Type: {Type: {Type: MoreERC4626CollateralConfig}}} /// "moetPreswapConfigs" → {Type: {Type: MoetPreswapConfig}} /// "originalCollateralTypes" → {UInt64: Type} /// "collateralPreSwappers" → {UInt64: {DeFiActions.Swapper}} @@ -181,37 +179,6 @@ access(all) contract FlowYieldVaultsStrategiesV2 { } } - /// Collateral configuration for strategies that borrow the vault's underlying asset directly, - /// using a standard ERC4626 deposit for the forward path (underlying → yield token) and a - /// UniV3 AMM swap for the reverse path (yield token → underlying). This applies to "More" - /// ERC4626 vaults that do not support synchronous redemptions via ERC4626 redeem(). - access(all) struct MoreERC4626CollateralConfig { - access(all) let yieldTokenEVMAddress: EVM.EVMAddress - /// UniV3 path for swapping yield token → underlying asset (used for debt repayment and - /// AutoBalancer rebalancing). The path must start with the yield token EVM address. - access(all) let yieldToUnderlyingUniV3AddressPath: [EVM.EVMAddress] - access(all) let yieldToUnderlyingUniV3FeePath: [UInt32] - /// UniV3 path for swapping debt token → collateral (used to convert overpayment dust - /// returned by position.closePosition back into collateral). The path must start with - /// the debt token EVM address and end with the collateral EVM address. - access(all) let debtToCollateralUniV3AddressPath: [EVM.EVMAddress] - access(all) let debtToCollateralUniV3FeePath: [UInt32] - - init( - yieldTokenEVMAddress: EVM.EVMAddress, - yieldToUnderlyingUniV3AddressPath: [EVM.EVMAddress], - yieldToUnderlyingUniV3FeePath: [UInt32], - debtToCollateralUniV3AddressPath: [EVM.EVMAddress], - debtToCollateralUniV3FeePath: [UInt32] - ) { - self.yieldTokenEVMAddress = yieldTokenEVMAddress - self.yieldToUnderlyingUniV3AddressPath = yieldToUnderlyingUniV3AddressPath - self.yieldToUnderlyingUniV3FeePath = yieldToUnderlyingUniV3FeePath - self.debtToCollateralUniV3AddressPath = debtToCollateralUniV3AddressPath - self.debtToCollateralUniV3FeePath = debtToCollateralUniV3FeePath - } - } - /// This strategy uses FUSDEV vault (Morpho ERC4626). /// Deposits collateral into a single FlowALP position, borrowing MOET as debt. /// MOET is swapped to PYUSD0 and deposited into the Morpho FUSDEV ERC4626 vault. @@ -518,313 +485,6 @@ access(all) contract FlowYieldVaultsStrategiesV2 { } } - /// This strategy uses syWFLOWv vault (More ERC4626). - /// Deposits collateral (non-FLOW) into a single FlowALP position, borrowing FLOW as debt. - /// Borrowed FLOW is deposited directly into the syWFLOWv More ERC4626 vault (no AMM swap needed - /// since FLOW is the vault's underlying asset). - /// FLOW (the vault's underlying asset) cannot be used as collateral for this strategy. - access(all) resource syWFLOWvStrategy : FlowYieldVaults.Strategy, DeFiActions.IdentifiableResource { - /// An optional identifier allowing protocols to identify stacked connector operations by defining a protocol- - /// specific Identifier to associated connectors on construction - access(contract) var uniqueID: DeFiActions.UniqueIdentifier? - access(self) let position: @FlowALPv0.Position - access(self) var sink: {DeFiActions.Sink} - access(self) var source: {DeFiActions.Source} - // debtTokenType moved to contract-level config["syWFLOWvDebtTokenTypes"] keyed by uniqueID.id - /// Swapper used in closePosition to source FLOW from the AutoBalancer (syWFLOWv → FLOW via UniV3). - access(self) let yieldToDebtSwapper: {DeFiActions.Swapper} - /// Swapper used in closePosition to convert FLOW overpayment dust back to collateral (FLOW → collateral via UniV3). - access(self) let debtToCollateralSwapper: {DeFiActions.Swapper} - /// Tracks whether the underlying FlowALP position has been closed. Once true, - /// availableBalance() returns 0.0 to avoid panicking when the pool no longer - /// holds the position (e.g. during YieldVault burnCallback after close). - access(self) var positionClosed: Bool - - init( - id: DeFiActions.UniqueIdentifier, - collateralType: Type, - position: @FlowALPv0.Position, - yieldToDebtSwapper: {DeFiActions.Swapper}, - debtToCollateralSwapper: {DeFiActions.Swapper} - ) { - self.uniqueID = id - self.sink = position.createSink(type: collateralType) - self.source = position.createSourceWithOptions(type: collateralType, pullFromTopUpSource: true) - self.yieldToDebtSwapper = yieldToDebtSwapper - self.debtToCollateralSwapper = debtToCollateralSwapper - self.positionClosed = false - self.position <-position - } - - // Inherited from FlowYieldVaults.Strategy default implementation - // access(all) view fun isSupportedCollateralType(_ type: Type): Bool - - access(all) view fun getSupportedCollateralTypes(): {Type: Bool} { - // If this strategy was initialized with a stablecoin pre-swap (e.g. PYUSD0→MOET), - // expose the original (external) collateral type to callers, not the internal MOET type. - if let id = self.uniqueID { - if let originalType = FlowYieldVaultsStrategiesV2._getOriginalCollateralType(id.id) { - return { originalType: true } - } - } - return { self.sink.getSinkType(): true } - } - /// Returns the amount available for withdrawal via the inner Source - access(all) fun availableBalance(ofToken: Type): UFix64 { - if self.positionClosed { return 0.0 } - // If stablecoin pre-swap is in effect, match against the original (external) collateral type. - // MOET and the stablecoin collateral (e.g. PYUSD0) have approximately equal value (1:1). - var effectiveSourceType = self.source.getSourceType() - if let id = self.uniqueID { - if let originalType = FlowYieldVaultsStrategiesV2._getOriginalCollateralType(id.id) { - effectiveSourceType = originalType - } - } - return ofToken == effectiveSourceType ? self.source.minimumAvailable() : 0.0 - } - /// Deposits up to the inner Sink's capacity from the provided authorized Vault reference. - /// Accepts both the internal collateral type and, when a pre-swap is configured, the original - /// external collateral type (e.g. PYUSD0) — which is swapped to MOET first. - /// FLOW cannot be used as collateral — it is the vault's underlying asset (the debt token). - access(all) fun deposit(from: auth(FungibleToken.Withdraw) &{FungibleToken.Vault}) { - pre { - from.getType() == self.sink.getSinkType() - || (self.uniqueID != nil && FlowYieldVaultsStrategiesV2._getOriginalCollateralType(self.uniqueID!.id) == from.getType()): - "syWFLOWvStrategy position only accepts \(self.sink.getSinkType().identifier) as collateral, got \(from.getType().identifier)" - } - // Reject the debt token (FLOW) as collateral — looked up from contract-level config - if let id = self.uniqueID { - let debtTokenType = FlowYieldVaultsStrategiesV2._getSyWFLOWvDebtTokenType(id.id) - assert( - debtTokenType == nil || from.getType() != debtTokenType!, - message: "syWFLOWvStrategy: FLOW cannot be used as collateral — it is the vault's underlying asset" - ) - } - // If depositing the original stablecoin collateral (e.g. PYUSD0), pre-swap to MOET - if let id = self.uniqueID { - if from.getType() != self.sink.getSinkType() { - if let preSwapper = FlowYieldVaultsStrategiesV2._getCollateralPreSwapper(id.id) { - let incoming <- from.withdraw(amount: from.balance) - if incoming.balance > 0.0 { - let quote = preSwapper.quoteOut(forProvided: incoming.balance, reverse: false) - if quote.outAmount > 0.0 { - let moetVault <- preSwapper.swap(quote: quote, inVault: <-incoming) - self.sink.depositCapacity(from: &moetVault as auth(FungibleToken.Withdraw) &{FungibleToken.Vault}) - Burner.burn(<-moetVault) - } else { - Burner.burn(<-incoming) - } - } else { - Burner.burn(<-incoming) - } - return - } - } - } - self.sink.depositCapacity(from: from) - } - /// Withdraws up to the max amount, returning the withdrawn Vault. If the requested token type is unsupported, - /// an empty Vault is returned. - /// - /// For the PYUSD0 pre-swap case: the internal source type is MOET but the external collateral - /// type is PYUSD0. We convert MOET→PYUSD0 via the moetToCollateralSwapper. - access(FungibleToken.Withdraw) fun withdraw(maxAmount: UFix64, ofToken: Type): @{FungibleToken.Vault} { - let effectiveSourceType = self.source.getSourceType() - if ofToken != effectiveSourceType { - // For pre-swap case: ofToken is external collateral (e.g. PYUSD0), source is MOET. - if let id = self.uniqueID { - if let originalType = FlowYieldVaultsStrategiesV2._getOriginalCollateralType(id.id) { - if ofToken == originalType { - if let moetToOrigSwapper = FlowYieldVaultsStrategiesV2._getMoetToCollateralSwapper(id.id) { - // Quote MOET in to get maxAmount PYUSD0 out - let quote = moetToOrigSwapper.quoteIn(forDesired: maxAmount, reverse: false) - if quote.inAmount > 0.0 { - let moetVault <- self.source.withdrawAvailable(maxAmount: quote.inAmount) - if moetVault.balance > 0.0 { - return <- moetToOrigSwapper.swap(quote: nil, inVault: <-moetVault) - } - Burner.burn(<-moetVault) - } - } - } - } - } - return <- DeFiActionsUtils.getEmptyVault(ofToken) - } - return <- self.source.withdrawAvailable(maxAmount: maxAmount) - } - /// Closes the underlying FlowALP position by preparing FLOW repayment funds from AutoBalancer - /// (via the stored yield→FLOW swapper) and closing with them. When a stablecoin pre-swap is - /// configured (e.g. PYUSD0→MOET), the returned MOET collateral is swapped back to the - /// original stablecoin before returning. - access(FungibleToken.Withdraw) fun closePosition(collateralType: Type): @{FungibleToken.Vault} { - pre { - self.isSupportedCollateralType(collateralType): - "Unsupported collateral type \(collateralType.identifier)" - } - post { - 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() // MOET - } - } - - // Step 1: Get debt amounts - let debtsByType = self.position.getTotalDebt() - - assert( - debtsByType.length <= 1, - message: "syWFLOWvStrategy position must have at most one debt type, found \(debtsByType.length)" - ) - - var totalDebtAmount: UFix64 = 0.0 - for debtAmount in debtsByType.values { - totalDebtAmount = totalDebtAmount + debtAmount - } - - // Step 2: If no debt, close with empty sources array - if totalDebtAmount == 0.0 { - let resultVaults <- self.position.closePosition(repaymentSources: []) - assert( - resultVaults.length <= 1, - message: "Expected 0 or 1 collateral vault from closePosition, got \(resultVaults.length)" - ) - if resultVaults.length == 0 { - destroy resultVaults - self.positionClosed = true - return <- DeFiActionsUtils.getEmptyVault(collateralType) - } - var collateralVault <- resultVaults.removeFirst() - destroy resultVaults - // Convert internal collateral (MOET) → external collateral (e.g. PYUSD0) if needed - 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) - self.positionClosed = true - return <- extVault - } - } - Burner.burn(<-collateralVault) - self.positionClosed = true - return <- DeFiActionsUtils.getEmptyVault(collateralType) - } - } - Burner.burn(<-collateralVault) - self.positionClosed = true - return <- DeFiActionsUtils.getEmptyVault(collateralType) - } - self.positionClosed = true - return <- collateralVault - } - - // Step 3: Create external syWFLOWv source from AutoBalancer - let yieldTokenSource = FlowYieldVaultsAutoBalancers.createExternalSource(id: self.id()!) - ?? panic("Could not create external source from AutoBalancer") - - // Step 4: Create a SwapSource that converts syWFLOWv → FLOW for debt repayment - let flowSource = SwapConnectors.SwapSource( - swapper: self.yieldToDebtSwapper, - source: yieldTokenSource, - uniqueID: self.copyID() - ) - - // Step 5: Close position — pool pulls exactly the FLOW debt amount from flowSource - let resultVaults <- self.position.closePosition(repaymentSources: [flowSource]) - - assert( - resultVaults.length >= 1 && resultVaults.length <= 2, - 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. - while resultVaults.length > 0 { - let dustVault <- resultVaults.removeFirst() - if dustVault.balance > 0.0 { - if dustVault.getType() == internalCollateralType { - collateralVault.deposit(from: <-dustVault) - } 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) - } - } - } else { - Burner.burn(<-dustVault) - } - } - - destroy resultVaults - - // Convert internal collateral (MOET) → external collateral (e.g. PYUSD0) if needed - 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) - self.positionClosed = true - return <- extVault - } - } - } - } - Burner.burn(<-collateralVault) - self.positionClosed = true - return <- DeFiActionsUtils.getEmptyVault(collateralType) - } - - self.positionClosed = true - return <- collateralVault - } - /// Executed when a Strategy is burned, cleaning up the Strategy's stored AutoBalancer and contract-level config entries - access(contract) fun burnCallback() { - FlowYieldVaultsAutoBalancers._cleanupAutoBalancer(id: self.id()!) - FlowYieldVaultsStrategiesV2._removeSyWFLOWvDebtTokenType(self.uniqueID?.id) - // Clean up stablecoin pre-swap config entries (no-op if not set) - if let id = self.uniqueID { - FlowYieldVaultsStrategiesV2._removeOriginalCollateralType(id.id) - FlowYieldVaultsStrategiesV2._removeCollateralPreSwapper(id.id) - FlowYieldVaultsStrategiesV2._removeMoetToCollateralSwapper(id.id) - } - } - access(all) fun getComponentInfo(): DeFiActions.ComponentInfo { - return DeFiActions.ComponentInfo( - type: self.getType(), - id: self.id(), - innerComponents: [ - self.sink.getComponentInfo(), - self.source.getComponentInfo() - ] - ) - } - access(contract) view fun copyID(): DeFiActions.UniqueIdentifier? { - return self.uniqueID - } - access(contract) fun setID(_ id: DeFiActions.UniqueIdentifier?) { - self.uniqueID = id - } - } - access(all) struct TokenBundle { /// The MOET token type (the pool's borrowable token) access(all) let moetTokenType: Type @@ -1460,286 +1120,6 @@ access(all) contract FlowYieldVaultsStrategiesV2 { } } - /// This StrategyComposer builds strategies that borrow the ERC4626 vault's own underlying - /// asset as debt (e.g. FLOW for syWFLOWv), depositing it directly via ERC4626 deposit/redeem - /// with no AMM swaps. FLOW (the underlying) cannot be used as collateral. - access(all) resource MoreERC4626StrategyComposer : FlowYieldVaults.StrategyComposer { - /// { Strategy Type: { Collateral Type: MoreERC4626CollateralConfig } } - access(self) let config: {Type: {Type: MoreERC4626CollateralConfig}} - - init(_ config: {Type: {Type: MoreERC4626CollateralConfig}}) { - self.config = config - } - - access(all) view fun getComposedStrategyTypes(): {Type: Bool} { - let composed: {Type: Bool} = {} - for t in self.config.keys { - composed[t] = true - } - return composed - } - - access(all) view fun getSupportedInitializationVaults(forStrategy: Type): {Type: Bool} { - let supported: {Type: Bool} = {} - if let strategyConfig = self.config[forStrategy] { - for collateralType in strategyConfig.keys { - supported[collateralType] = true - } - } - return supported - } - - access(self) view fun _supportsCollateral(forStrategy: Type, collateral: Type): Bool { - if let strategyConfig = self.config[forStrategy] { - return strategyConfig[collateral] != nil - } - return false - } - - access(all) view fun getSupportedInstanceVaults(forStrategy: Type, initializedWith: Type): {Type: Bool} { - return self._supportsCollateral(forStrategy: forStrategy, collateral: initializedWith) - ? { initializedWith: true } - : {} - } - - access(all) fun createStrategy( - _ type: Type, - uniqueID: DeFiActions.UniqueIdentifier, - withFunds: @{FungibleToken.Vault} - ): @{FlowYieldVaults.Strategy} { - pre { - self.config[type] != nil: "Unsupported strategy type \(type.identifier)" - } - - switch type { - case Type<@syWFLOWvStrategy>(): - let collateralType = withFunds.getType() - - let stratConfig = self.config[Type<@syWFLOWvStrategy>()] - ?? panic("Could not find config for strategy syWFLOWvStrategy") - let collateralConfig = stratConfig[collateralType] - ?? panic("Could not find config for collateral \(collateralType.identifier) when creating syWFLOWvStrategy") - - let tokens = FlowYieldVaultsStrategiesV2._resolveTokenBundle( - yieldTokenEVMAddress: collateralConfig.yieldTokenEVMAddress - ) - - // Reject FLOW as collateral — it is the vault's underlying / debt token - assert( - collateralType != tokens.underlying4626AssetType, - message: "syWFLOWvStrategy: FLOW cannot be used as collateral — it is the vault's underlying asset" - ) - - let yieldTokenOracle = FlowYieldVaultsStrategiesV2._createYieldTokenOracle( - yieldTokenEVMAddress: tokens.yieldTokenEVMAddress, - underlyingAssetType: tokens.underlying4626AssetType, - uniqueID: uniqueID - ) - - let recurringConfig = FlowYieldVaultsStrategiesV2._createRecurringConfig(withID: uniqueID) - - let balancerIO = FlowYieldVaultsStrategiesV2._initAutoBalancerAndIO( - oracle: yieldTokenOracle, - yieldTokenType: tokens.yieldTokenType, - recurringConfig: recurringConfig, - uniqueID: uniqueID - ) - - // For syWFLOWvStrategy the debt token IS the underlying asset (FLOW), not MOET. - let flowDebtTokenType = tokens.underlying4626AssetType - - // FLOW → syWFLOWv: standard ERC4626 deposit (More vault, not Morpho — no AMM needed) - let flowToSyWFLOWv = ERC4626SwapConnectors.Swapper( - asset: tokens.underlying4626AssetType, - vault: tokens.yieldTokenEVMAddress, - coa: FlowYieldVaultsStrategiesV2._getCOACapability(), - feeSource: FlowYieldVaultsStrategiesV2._createFeeSource(withID: uniqueID), - uniqueID: uniqueID - ) - // syWFLOWv → FLOW: UniV3 AMM swap (More vault does not support synchronous ERC4626 redeem) - let syWFLOWvToFlow = UniswapV3SwapConnectors.Swapper( - factoryAddress: FlowYieldVaultsStrategiesV2.univ3FactoryEVMAddress, - routerAddress: FlowYieldVaultsStrategiesV2.univ3RouterEVMAddress, - quoterAddress: FlowYieldVaultsStrategiesV2.univ3QuoterEVMAddress, - tokenPath: collateralConfig.yieldToUnderlyingUniV3AddressPath, - feePath: collateralConfig.yieldToUnderlyingUniV3FeePath, - inVault: tokens.yieldTokenType, - outVault: tokens.underlying4626AssetType, - coaCapability: FlowYieldVaultsStrategiesV2._getCOACapability(), - uniqueID: uniqueID - ) - - // issuanceSink: pool pushes borrowed FLOW → deposit → syWFLOWv → AutoBalancer - let abaSwapSinkFlow = SwapConnectors.SwapSink( - swapper: flowToSyWFLOWv, - sink: balancerIO.sink, - uniqueID: uniqueID - ) - // repaymentSource: AutoBalancer → syWFLOWv → AMM swap → FLOW → pool - let abaSwapSourceFlow = SwapConnectors.SwapSource( - swapper: syWFLOWvToFlow, - source: balancerIO.source, - uniqueID: uniqueID - ) - - // --- Stablecoin pre-swap path (e.g. PYUSD0 → MOET) --- - if let preswapCfg = FlowYieldVaultsStrategiesV2._getMoetPreswapConfig( - composer: Type<@MoreERC4626StrategyComposer>(), - collateral: collateralType - ) { - let preSwapper = UniswapV3SwapConnectors.Swapper( - factoryAddress: FlowYieldVaultsStrategiesV2.univ3FactoryEVMAddress, - routerAddress: FlowYieldVaultsStrategiesV2.univ3RouterEVMAddress, - quoterAddress: FlowYieldVaultsStrategiesV2.univ3QuoterEVMAddress, - tokenPath: preswapCfg.collateralToMoetAddressPath, - feePath: preswapCfg.collateralToMoetFeePath, - inVault: collateralType, - outVault: tokens.moetTokenType, - coaCapability: FlowYieldVaultsStrategiesV2._getCOACapability(), - uniqueID: uniqueID - ) - let preSwapQuote = preSwapper.quoteOut(forProvided: withFunds.balance, reverse: false) - let moetFunds <- preSwapper.swap(quote: preSwapQuote, inVault: <-withFunds) - - // Open FlowALP position with MOET as collateral - let positionPreswap <- FlowYieldVaultsStrategiesV2._openCreditPosition( - funds: <-moetFunds, - issuanceSink: abaSwapSinkFlow, - repaymentSource: abaSwapSourceFlow - ) - - // AutoBalancer debt management: same as WBTC/WETH (manages FLOW borrow/repay) - let positionDebtSinkPre = positionPreswap.createSinkWithOptions( - type: flowDebtTokenType, - pushToDrawDownSink: false - ) - let positionDebtSwapSinkPre = SwapConnectors.SwapSink( - swapper: syWFLOWvToFlow, - sink: positionDebtSinkPre, - uniqueID: uniqueID - ) - let positionDebtSourcePre = positionPreswap.createSourceWithOptions( - type: flowDebtTokenType, - pullFromTopUpSource: false - ) - let positionDebtSwapSourcePre = SwapConnectors.SwapSource( - swapper: flowToSyWFLOWv, - source: positionDebtSourcePre, - uniqueID: uniqueID - ) - balancerIO.autoBalancer.setSink(positionDebtSwapSinkPre, updateSinkID: true) - balancerIO.autoBalancer.setSource(positionDebtSwapSourcePre, updateSourceID: true) - - // FLOW→MOET: converts FLOW dust back to MOET (internal collateral) in closePosition. - // Path: FLOW → collateral (debtToCollateral) → MOET (pre-swap reversed hop). - // e.g. for PYUSD0: WFLOW→(fee 500)→PYUSD0→(fee 100)→MOET (2-hop). - var flowToMoetPath = collateralConfig.debtToCollateralUniV3AddressPath - flowToMoetPath.append(preswapCfg.collateralToMoetAddressPath[preswapCfg.collateralToMoetAddressPath.length - 1]) - var flowToMoetFees = collateralConfig.debtToCollateralUniV3FeePath - flowToMoetFees.append(preswapCfg.collateralToMoetFeePath[preswapCfg.collateralToMoetFeePath.length - 1]) - let flowToMoet = UniswapV3SwapConnectors.Swapper( - factoryAddress: FlowYieldVaultsStrategiesV2.univ3FactoryEVMAddress, - routerAddress: FlowYieldVaultsStrategiesV2.univ3RouterEVMAddress, - quoterAddress: FlowYieldVaultsStrategiesV2.univ3QuoterEVMAddress, - tokenPath: flowToMoetPath, - feePath: flowToMoetFees, - inVault: tokens.underlying4626AssetType, // FLOW - outVault: tokens.moetTokenType, // MOET - coaCapability: FlowYieldVaultsStrategiesV2._getCOACapability(), - uniqueID: uniqueID - ) - - // MOET→collateral (e.g. PYUSD0): final conversion in closePosition - let moetToCollateral = UniswapV3SwapConnectors.Swapper( - factoryAddress: FlowYieldVaultsStrategiesV2.univ3FactoryEVMAddress, - routerAddress: FlowYieldVaultsStrategiesV2.univ3RouterEVMAddress, - quoterAddress: FlowYieldVaultsStrategiesV2.univ3QuoterEVMAddress, - tokenPath: preswapCfg.collateralToMoetAddressPath.reverse(), - feePath: preswapCfg.collateralToMoetFeePath.reverse(), - inVault: tokens.moetTokenType, - outVault: collateralType, - coaCapability: FlowYieldVaultsStrategiesV2._getCOACapability(), - uniqueID: uniqueID - ) - - FlowYieldVaultsStrategiesV2._setSyWFLOWvDebtTokenType(uniqueID.id, flowDebtTokenType) - FlowYieldVaultsStrategiesV2._setOriginalCollateralType(uniqueID.id, collateralType) - FlowYieldVaultsStrategiesV2._setCollateralPreSwapper(uniqueID.id, preSwapper) - FlowYieldVaultsStrategiesV2._setMoetToCollateralSwapper(uniqueID.id, moetToCollateral) - - return <-create syWFLOWvStrategy( - id: uniqueID, - collateralType: tokens.moetTokenType, - position: <-positionPreswap, - yieldToDebtSwapper: syWFLOWvToFlow, - debtToCollateralSwapper: flowToMoet - ) - } - - // --- Standard path (WBTC, WETH — directly supported by FlowALP) --- - - // Open FlowALP position with collateral; drawDownSink accepts FLOW - let positionFlow <- FlowYieldVaultsStrategiesV2._openCreditPosition( - funds: <-withFunds, - issuanceSink: abaSwapSinkFlow, - repaymentSource: abaSwapSourceFlow - ) - - // AutoBalancer overflow: excess syWFLOWv → AMM swap → FLOW → repay position debt - let positionDebtSink = positionFlow.createSinkWithOptions( - type: flowDebtTokenType, - pushToDrawDownSink: false - ) - let positionDebtSwapSink = SwapConnectors.SwapSink( - swapper: syWFLOWvToFlow, - sink: positionDebtSink, - uniqueID: uniqueID - ) - - // AutoBalancer deficit: borrow more FLOW from position → deposit → syWFLOWv - let positionDebtSource = positionFlow.createSourceWithOptions( - type: flowDebtTokenType, - pullFromTopUpSource: false - ) - let positionDebtSwapSource = SwapConnectors.SwapSource( - swapper: flowToSyWFLOWv, - source: positionDebtSource, - uniqueID: uniqueID - ) - - balancerIO.autoBalancer.setSink(positionDebtSwapSink, updateSinkID: true) - balancerIO.autoBalancer.setSource(positionDebtSwapSource, updateSourceID: true) - - // FLOW→collateral swapper for dust conversion in closePosition - let flowToCollateral = UniswapV3SwapConnectors.Swapper( - factoryAddress: FlowYieldVaultsStrategiesV2.univ3FactoryEVMAddress, - routerAddress: FlowYieldVaultsStrategiesV2.univ3RouterEVMAddress, - quoterAddress: FlowYieldVaultsStrategiesV2.univ3QuoterEVMAddress, - tokenPath: collateralConfig.debtToCollateralUniV3AddressPath, - feePath: collateralConfig.debtToCollateralUniV3FeePath, - inVault: tokens.underlying4626AssetType, - outVault: collateralType, - coaCapability: FlowYieldVaultsStrategiesV2._getCOACapability(), - uniqueID: uniqueID - ) - - // Store debtTokenType in contract-level config (resource field removed for upgrade compat) - FlowYieldVaultsStrategiesV2._setSyWFLOWvDebtTokenType(uniqueID.id, flowDebtTokenType) - - return <-create syWFLOWvStrategy( - id: uniqueID, - collateralType: collateralType, - position: <-positionFlow, - yieldToDebtSwapper: syWFLOWvToFlow, - debtToCollateralSwapper: flowToCollateral - ) - - default: - panic("Unsupported strategy type \(type.identifier)") - } - } - } - access(all) entitlement Configure access(self) @@ -1764,35 +1144,6 @@ access(all) contract FlowYieldVaultsStrategiesV2 { ) } - access(self) - fun makeMoreERC4626CollateralConfig( - yieldTokenEVMAddress: EVM.EVMAddress, - yieldToUnderlyingAddressPath: [EVM.EVMAddress], - yieldToUnderlyingFeePath: [UInt32], - debtToCollateralAddressPath: [EVM.EVMAddress], - debtToCollateralFeePath: [UInt32] - ): MoreERC4626CollateralConfig { - pre { - yieldToUnderlyingAddressPath.length > 1: - "Invalid Uniswap V3 swap path length" - yieldToUnderlyingFeePath.length == yieldToUnderlyingAddressPath.length - 1: - "Uniswap V3 fee path length must be path length - 1" - yieldToUnderlyingAddressPath[0].equals(yieldTokenEVMAddress): - "UniswapV3 swap path must start with yield token" - debtToCollateralAddressPath.length > 1: - "Invalid debt-to-collateral Uniswap V3 path length" - debtToCollateralFeePath.length == debtToCollateralAddressPath.length - 1: - "Debt-to-collateral Uniswap V3 fee path length must be path length - 1" - } - return MoreERC4626CollateralConfig( - yieldTokenEVMAddress: yieldTokenEVMAddress, - yieldToUnderlyingUniV3AddressPath: yieldToUnderlyingAddressPath, - yieldToUnderlyingUniV3FeePath: yieldToUnderlyingFeePath, - debtToCollateralUniV3AddressPath: debtToCollateralAddressPath, - debtToCollateralUniV3FeePath: debtToCollateralFeePath - ) - } - /// This resource enables the issuance of StrategyComposers, thus safeguarding the issuance of Strategies which /// may utilize resource consumption (i.e. account storage). Since Strategy creation consumes account storage /// via configured AutoBalancers @@ -1815,21 +1166,17 @@ access(all) contract FlowYieldVaultsStrategiesV2 { if stratPartition[collateral] != nil { return true } } } - return FlowYieldVaultsStrategiesV2._getMoreERC4626Config( - composer: composer, strategy: strategy, collateral: collateral - ) != nil + return false } access(all) view fun getSupportedComposers(): {Type: Bool} { return { Type<@MorphoERC4626StrategyComposer>(): true, - Type<@MoreERC4626StrategyComposer>(): true } } access(self) view fun isSupportedComposer(_ type: Type): Bool { return type == Type<@MorphoERC4626StrategyComposer>() - || type == Type<@MoreERC4626StrategyComposer>() } access(all) fun issueComposer(_ type: Type): @{FlowYieldVaults.StrategyComposer} { @@ -1839,10 +1186,6 @@ access(all) contract FlowYieldVaultsStrategiesV2 { switch type { case Type<@MorphoERC4626StrategyComposer>(): return <- create MorphoERC4626StrategyComposer(self.configs[type] ?? {}) - case Type<@MoreERC4626StrategyComposer>(): - return <- create MoreERC4626StrategyComposer( - FlowYieldVaultsStrategiesV2._getMoreERC4626ComposerConfig(type) - ) default: panic("Unsupported StrategyComposer \(type.identifier) requested") } @@ -1875,34 +1218,6 @@ access(all) contract FlowYieldVaultsStrategiesV2 { self.configs[composerType] = composerPartition } - /// Merges new MoreERC4626CollateralConfig entries into the MoreERC4626StrategyComposer config. - access(Configure) - fun upsertMoreERC4626Config( - config: {Type: {Type: FlowYieldVaultsStrategiesV2.MoreERC4626CollateralConfig}} - ) { - for stratType in config.keys { - assert(stratType.isSubtype(of: Type<@{FlowYieldVaults.Strategy}>()), - message: "Invalid config key \(stratType.identifier) - not a FlowYieldVaults.Strategy Type") - for collateralType in config[stratType]!.keys { - assert(collateralType.isSubtype(of: Type<@{FungibleToken.Vault}>()), - message: "Invalid config key at config[\(stratType.identifier)] - \(collateralType.identifier) is not a FungibleToken.Vault") - } - } - - let composerType = Type<@MoreERC4626StrategyComposer>() - for stratType in config.keys { - let newPerCollateral = config[stratType]! - for collateralType in newPerCollateral.keys { - FlowYieldVaultsStrategiesV2._setMoreERC4626Config( - composer: composerType, - strategy: stratType, - collateral: collateralType, - cfg: newPerCollateral[collateralType]! - ) - } - } - } - access(Configure) fun addOrUpdateMorphoCollateralConfig( strategyType: Type, collateralVaultType: Type, @@ -1925,32 +1240,6 @@ access(all) contract FlowYieldVaultsStrategiesV2 { self.upsertMorphoConfig(config: { strategyType: { collateralVaultType: base } }) } - access(Configure) fun addOrUpdateMoreERC4626CollateralConfig( - strategyType: Type, - collateralVaultType: Type, - yieldTokenEVMAddress: EVM.EVMAddress, - yieldToUnderlyingAddressPath: [EVM.EVMAddress], - yieldToUnderlyingFeePath: [UInt32], - debtToCollateralAddressPath: [EVM.EVMAddress], - debtToCollateralFeePath: [UInt32] - ) { - pre { - strategyType.isSubtype(of: Type<@{FlowYieldVaults.Strategy}>()): - "Strategy type \(strategyType.identifier) is not a FlowYieldVaults.Strategy" - collateralVaultType.isSubtype(of: Type<@{FungibleToken.Vault}>()): - "Collateral type \(collateralVaultType.identifier) is not a FungibleToken.Vault" - } - - let cfg = FlowYieldVaultsStrategiesV2.makeMoreERC4626CollateralConfig( - yieldTokenEVMAddress: yieldTokenEVMAddress, - yieldToUnderlyingAddressPath: yieldToUnderlyingAddressPath, - yieldToUnderlyingFeePath: yieldToUnderlyingFeePath, - debtToCollateralAddressPath: debtToCollateralAddressPath, - debtToCollateralFeePath: debtToCollateralFeePath - ) - self.upsertMoreERC4626Config(config: { strategyType: { collateralVaultType: cfg } }) - } - /// Configures a stablecoin collateral type to use MOET pre-swap before depositing into /// FlowALP. Required for stablecoins like PYUSD0 that FlowALP does not support directly /// as collateral (it only accepts MOET as its stablecoin). @@ -1964,9 +1253,8 @@ access(all) contract FlowYieldVaultsStrategiesV2 { collateralToMoetFeePath: [UInt32] ) { pre { - composer == Type<@MorphoERC4626StrategyComposer>() - || composer == Type<@MoreERC4626StrategyComposer>(): - "composer must be MorphoERC4626StrategyComposer or MoreERC4626StrategyComposer" + composer == Type<@MorphoERC4626StrategyComposer>(): + "composer must be MorphoERC4626StrategyComposer" collateralVaultType.isSubtype(of: Type<@{FungibleToken.Vault}>()): "collateralVaultType must be a FungibleToken.Vault" collateralToMoetAddressPath.length > 1: @@ -1990,7 +1278,6 @@ access(all) contract FlowYieldVaultsStrategiesV2 { Type<@FUSDEVStrategy>(): {} as {Type: CollateralConfig} } } - FlowYieldVaultsStrategiesV2._purgeMoreERC4626Configs() FlowYieldVaultsStrategiesV2.config["moetPreswapConfigs"] = {} as {Type: {Type: FlowYieldVaultsStrategiesV2.MoetPreswapConfig}} } } @@ -2129,76 +1416,6 @@ access(all) contract FlowYieldVaultsStrategiesV2 { } } - // --- "syWFLOWvDebtTokenTypes" partition --- - // Stores the debt token Type per syWFLOWvStrategy uniqueID. - // Kept in the contract-level config map so no new field is added to the deployed syWFLOWvStrategy resource. - - access(contract) view fun _getSyWFLOWvDebtTokenType(_ id: UInt64): Type? { - let partition = FlowYieldVaultsStrategiesV2.config["syWFLOWvDebtTokenTypes"] as! {UInt64: Type}? ?? {} - return partition[id] - } - - access(contract) fun _setSyWFLOWvDebtTokenType(_ id: UInt64, _ t: Type) { - var partition = FlowYieldVaultsStrategiesV2.config["syWFLOWvDebtTokenTypes"] as! {UInt64: Type}? ?? {} - partition[id] = t - FlowYieldVaultsStrategiesV2.config["syWFLOWvDebtTokenTypes"] = partition - } - - access(contract) fun _removeSyWFLOWvDebtTokenType(_ id: UInt64?) { - if let unwrappedID = id { - var partition = FlowYieldVaultsStrategiesV2.config["syWFLOWvDebtTokenTypes"] as! {UInt64: Type}? ?? {} - partition.remove(key: unwrappedID) - FlowYieldVaultsStrategiesV2.config["syWFLOWvDebtTokenTypes"] = partition - } - } - - // --- "moreERC4626Configs" partition --- - // Stores MoreERC4626CollateralConfig keyed by composer type → strategy type → collateral type. - // Kept in the contract-level config map so no new field is added to the deployed StrategyComposerIssuer resource. - - access(contract) view fun _getMoreERC4626Config( - composer: Type, - strategy: Type, - collateral: Type - ): MoreERC4626CollateralConfig? { - let partition = FlowYieldVaultsStrategiesV2.config["moreERC4626Configs"] - as! {Type: {Type: {Type: MoreERC4626CollateralConfig}}}? ?? {} - if let composerPart = partition[composer] { - if let stratPart = composerPart[strategy] { - return stratPart[collateral] - } - } - return nil - } - - access(contract) view fun _getMoreERC4626ComposerConfig( - _ composerType: Type - ): {Type: {Type: MoreERC4626CollateralConfig}} { - let partition = FlowYieldVaultsStrategiesV2.config["moreERC4626Configs"] - as! {Type: {Type: {Type: MoreERC4626CollateralConfig}}}? ?? {} - return partition[composerType] ?? {} - } - - access(contract) fun _setMoreERC4626Config( - composer: Type, - strategy: Type, - collateral: Type, - cfg: MoreERC4626CollateralConfig - ) { - var partition = FlowYieldVaultsStrategiesV2.config["moreERC4626Configs"] - as! {Type: {Type: {Type: MoreERC4626CollateralConfig}}}? ?? {} - var composerPartition = partition[composer] ?? {} - var stratPartition = composerPartition[strategy] ?? {} - stratPartition[collateral] = cfg - composerPartition[strategy] = stratPartition - partition[composer] = composerPartition - FlowYieldVaultsStrategiesV2.config["moreERC4626Configs"] = partition - } - - access(contract) fun _purgeMoreERC4626Configs() { - FlowYieldVaultsStrategiesV2.config["moreERC4626Configs"] = {} as {Type: {Type: {Type: MoreERC4626CollateralConfig}}} - } - // --- "originalCollateralTypes" partition --- // Stores the original (external) collateral type per strategy uniqueID when a MOET pre-swap // is in effect. E.g. PYUSD0 when the position internally holds MOET. @@ -2242,8 +1459,8 @@ access(all) contract FlowYieldVaultsStrategiesV2 { } // --- "moetToCollateralSwappers" partition --- - // Stores a MOET→original-collateral swapper per strategy uniqueID (FUSDEVStrategy and - // syWFLOWvStrategy). Built from the reversed MoetPreswapConfig path (same path in reverse). + // Stores a MOET→original-collateral swapper per strategy uniqueID (FUSDEVStrategy). + // Built from the reversed MoetPreswapConfig path (same path in reverse). // Used in closePosition to convert returned MOET collateral back to the original stablecoin // (e.g. PYUSD0) for the no-debt path. The regular close path uses debtToCollateralSwappers. From b7984d9c44d9903596465a9963dc77e221da3425 Mon Sep 17 00:00:00 2001 From: Alex <12097569+nialexsan@users.noreply.github.com> Date: Mon, 16 Mar 2026 11:09:10 -0400 Subject: [PATCH 03/29] isolated FUSDEV test from fyv-v2-strategy-deployment --- ...lowYieldVaultsStrategiesV2_FUSDEV_test.cdc | 562 ++++++++++++++++++ 1 file changed, 562 insertions(+) create mode 100644 cadence/tests/FlowYieldVaultsStrategiesV2_FUSDEV_test.cdc diff --git a/cadence/tests/FlowYieldVaultsStrategiesV2_FUSDEV_test.cdc b/cadence/tests/FlowYieldVaultsStrategiesV2_FUSDEV_test.cdc new file mode 100644 index 00000000..34c2c202 --- /dev/null +++ b/cadence/tests/FlowYieldVaultsStrategiesV2_FUSDEV_test.cdc @@ -0,0 +1,562 @@ +#test_fork(network: "mainnet", height: nil) + +import Test + +import "EVM" +import "FlowToken" +import "FlowYieldVaults" +import "FlowYieldVaultsClosedBeta" + +/// Fork test for FlowYieldVaultsStrategiesV2 FUSDEVStrategy. +/// +/// Tests the full YieldVault lifecycle (create, deposit, withdraw, close) for each supported +/// collateral type: WFLOW (FlowToken), WBTC, and WETH. +/// +/// PYUSD0 cannot be used as collateral — it is the FUSDEV vault's underlying asset. The +/// test setup intentionally omits a PYUSD0 collateral config so that negative tests can +/// assert the correct rejection. +/// +/// Strategy: +/// → FlowALP borrow MOET → swap MOET→PYUSD0 → ERC4626 deposit → FUSDEV (Morpho vault) +/// Close: FUSDEV → PYUSD0 (redeem) → MOET → repay FlowALP → returned to user +/// +/// Mainnet addresses: +/// - Admin (FlowYieldVaults deployer): 0xb1d63873c3cc9f79 +/// - WFLOW/PYUSD0 negative-test user: 0x443472749ebdaac8 (holds PYUSD0 and FLOW on mainnet) +/// - WBTC/WETH user: 0x68da18f20e98a7b6 (has ~12 WETH in EVM COA; WETH bridged + WBTC swapped in setup) +/// - UniV3 Factory: 0xca6d7Bb03334bBf135902e1d919a5feccb461632 +/// - UniV3 Router: 0xeEDC6Ff75e1b10B903D9013c358e446a73d35341 +/// - UniV3 Quoter: 0x370A8DF17742867a44e56223EC20D82092242C85 +/// - FUSDEV (Morpho ERC4626): 0xd069d989e2F44B70c65347d1853C0c67e10a9F8D +/// - PYUSD0: 0x99aF3EeA856556646C98c8B9b2548Fe815240750 +/// - WFLOW: 0xd3bF53DAC106A0290B0483EcBC89d40FcC961f3e +/// - WBTC: 0x717DAE2BaF7656BE9a9B01deE31d571a9d4c9579 (cbBTC; no WFLOW pool — use WETH as intermediate) +/// - WETH: 0x2F6F07CDcf3588944Bf4C42aC74ff24bF56e7590 + +// --- Accounts --- + +/// Mainnet admin account — deployer of FlowYieldVaults, FlowYieldVaultsClosedBeta, FlowYieldVaultsStrategiesV2 +access(all) let adminAccount = Test.getAccount(0xb1d63873c3cc9f79) + +/// WFLOW test user — holds FLOW (and PYUSD0) on mainnet. +/// Used for WFLOW lifecycle tests and for the negative PYUSD0 collateral test. +access(all) let flowUser = Test.getAccount(0x443472749ebdaac8) + +/// FlowToken contract account — used to provision FLOW to flowUser in setup. +access(all) let flowTokenAccount = Test.getAccount(0x1654653399040a61) + +/// WBTC/WETH holder — this account has ~12 WETH in its EVM COA on mainnet. +/// WETH is bridged to Cadence during setup(), and some WETH is then swapped → WBTC +/// via the UniV3 WETH/WBTC pool so that both collateral types can be tested. +/// COA EVM: 0x000000000000000000000002b87c966bc00bc2c4 +access(all) let wbtcUser = Test.getAccount(0x68da18f20e98a7b6) +access(all) let wethUser = Test.getAccount(0x68da18f20e98a7b6) + +// --- Strategy Config --- + +access(all) let fusdEvStrategyIdentifier = "A.b1d63873c3cc9f79.FlowYieldVaultsStrategiesV2.FUSDEVStrategy" +access(all) let composerIdentifier = "A.b1d63873c3cc9f79.FlowYieldVaultsStrategiesV2.MorphoERC4626StrategyComposer" +access(all) let issuerStoragePath: StoragePath = /storage/FlowYieldVaultsStrategyV2ComposerIssuer_0xb1d63873c3cc9f79 + +// --- Cadence Vault Type Identifiers --- + +/// FlowToken (WFLOW on EVM side) — used as WFLOW collateral +access(all) let flowVaultIdentifier = "A.1654653399040a61.FlowToken.Vault" +/// VM-bridged ERC-20 tokens +access(all) let wbtcVaultIdentifier = "A.1e4aa0b87d10b141.EVMVMBridgedToken_717dae2baf7656be9a9b01dee31d571a9d4c9579.Vault" +access(all) let wethVaultIdentifier = "A.1e4aa0b87d10b141.EVMVMBridgedToken_2f6f07cdcf3588944bf4c42ac74ff24bf56e7590.Vault" +access(all) let pyusd0VaultIdentifier = "A.1e4aa0b87d10b141.EVMVMBridgedToken_99af3eea856556646c98c8b9b2548fe815240750.Vault" + +// --- EVM Addresses --- + +access(all) let fusdEvEVMAddress = "0xd069d989e2F44B70c65347d1853C0c67e10a9F8D" +access(all) let pyusd0EVMAddress = "0x99aF3EeA856556646C98c8B9b2548Fe815240750" +access(all) let wflowEVMAddress = "0xd3bF53DAC106A0290B0483EcBC89d40FcC961f3e" +access(all) let wbtcEVMAddress = "0x717DAE2BaF7656BE9a9B01deE31d571a9d4c9579" +access(all) let wethEVMAddress = "0x2F6F07CDcf3588944Bf4C42aC74ff24bF56e7590" + +// --- Test State (vault IDs set during create tests, read by subsequent tests) --- + +access(all) var flowVaultID: UInt64 = 0 +access(all) var wbtcVaultID: UInt64 = 0 +access(all) var wethVaultID: UInt64 = 0 + +/* --- Helpers --- */ + +access(all) +fun _executeScript(_ path: String, _ args: [AnyStruct]): Test.ScriptResult { + return Test.executeScript(Test.readFile(path), args) +} + +access(all) +fun _executeTransactionFile(_ path: String, _ args: [AnyStruct], _ signers: [Test.TestAccount]): Test.TransactionResult { + let txn = Test.Transaction( + code: Test.readFile(path), + authorizers: signers.map(fun (s: Test.TestAccount): Address { return s.address }), + signers: signers, + arguments: args + ) + return Test.executeTransaction(txn) +} + +access(all) +fun equalAmounts(a: UFix64, b: UFix64, tolerance: UFix64): Bool { + if a > b { return a - b <= tolerance } + return b - a <= tolerance +} + +/// Returns the most-recently-created YieldVault ID for the given account. +access(all) +fun _latestVaultID(_ user: Test.TestAccount): UInt64 { + let r = _executeScript("../scripts/flow-yield-vaults/get_yield_vault_ids.cdc", [user.address]) + Test.expect(r, Test.beSucceeded()) + let ids = r.returnValue! as! [UInt64]? + Test.assert(ids != nil && ids!.length > 0, message: "Expected at least one yield vault for ".concat(user.address.toString())) + return ids![ids!.length - 1] +} + +/* --- Setup --- */ + +access(all) fun setup() { + log("==== FlowYieldVaultsStrategiesV2 FUSDEV Fork Test Setup ====") + + log("Deploying EVMAmountUtils...") + var err = Test.deployContract( + name: "EVMAmountUtils", + path: "../../lib/FlowALP/FlowActions/cadence/contracts/connectors/evm/EVMAmountUtils.cdc", + arguments: [] + ) + Test.expect(err, Test.beNil()) + + log("Deploying UniswapV3SwapConnectors...") + err = Test.deployContract( + name: "UniswapV3SwapConnectors", + path: "../../lib/FlowALP/FlowActions/cadence/contracts/connectors/evm/UniswapV3SwapConnectors.cdc", + arguments: [] + ) + Test.expect(err, Test.beNil()) + + log("Deploying ERC4626Utils...") + err = Test.deployContract( + name: "ERC4626Utils", + path: "../../lib/FlowALP/FlowActions/cadence/contracts/utils/ERC4626Utils.cdc", + arguments: [] + ) + Test.expect(err, Test.beNil()) + + log("Deploying ERC4626SwapConnectors...") + err = Test.deployContract( + name: "ERC4626SwapConnectors", + path: "../../lib/FlowALP/FlowActions/cadence/contracts/connectors/evm/ERC4626SwapConnectors.cdc", + arguments: [] + ) + Test.expect(err, Test.beNil()) + + // MorphoERC4626SinkConnectors must come before MorphoERC4626SwapConnectors (it imports it). + log("Deploying MorphoERC4626SinkConnectors...") + err = Test.deployContract( + name: "MorphoERC4626SinkConnectors", + path: "../../lib/FlowALP/FlowActions/cadence/contracts/connectors/evm/morpho/MorphoERC4626SinkConnectors.cdc", + arguments: [] + ) + Test.expect(err, Test.beNil()) + + log("Deploying MorphoERC4626SwapConnectors...") + err = Test.deployContract( + name: "MorphoERC4626SwapConnectors", + path: "../../lib/FlowALP/FlowActions/cadence/contracts/connectors/evm/morpho/MorphoERC4626SwapConnectors.cdc", + arguments: [] + ) + Test.expect(err, Test.beNil()) + + log("Deploying FlowYieldVaults...") + err = Test.deployContract( + name: "FlowYieldVaults", + path: "../../cadence/contracts/FlowYieldVaults.cdc", + arguments: [] + ) + Test.expect(err, Test.beNil()) + + log("Deploying ERC4626PriceOracles...") + err = Test.deployContract( + name: "ERC4626PriceOracles", + path: "../../lib/FlowALP/FlowActions/cadence/contracts/connectors/evm/ERC4626PriceOracles.cdc", + arguments: [] + ) + Test.expect(err, Test.beNil()) + + log("Deploying FlowALPv0...") + err = Test.deployContract( + name: "FlowALPv0", + path: "../../lib/FlowALP/cadence/contracts/FlowALPv0.cdc", + arguments: [] + ) + Test.expect(err, Test.beNil()) + + log("Deploying FlowYieldVaultsAutoBalancers...") + err = Test.deployContract( + name: "FlowYieldVaultsAutoBalancers", + path: "../../cadence/contracts/FlowYieldVaultsAutoBalancers.cdc", + arguments: [] + ) + Test.expect(err, Test.beNil()) + + log("Deploying FlowYieldVaultsStrategiesV2...") + err = Test.deployContract( + name: "FlowYieldVaultsStrategiesV2", + path: "../../cadence/contracts/FlowYieldVaultsStrategiesV2.cdc", + arguments: [ + "0xca6d7Bb03334bBf135902e1d919a5feccb461632", + "0xeEDC6Ff75e1b10B903D9013c358e446a73d35341", + "0x370A8DF17742867a44e56223EC20D82092242C85" + ] + ) + Test.expect(err, Test.beNil()) + + // Configure UniV3 paths for FUSDEVStrategy. + // Closing direction: FUSDEV → PYUSD0 (Morpho redeem, fee 100) → (UniV3 swap, fee 3000). + // PYUSD0 is intentionally NOT configured as collateral — it is the underlying asset. + + log("Configuring FUSDEVStrategy + WFLOW (FUSDEV→PYUSD0→WFLOW fees 100/3000)...") + result = _executeTransactionFile( + "../transactions/flow-yield-vaults/admin/upsert_strategy_config.cdc", + [ + fusdEvStrategyIdentifier, + flowVaultIdentifier, + fusdEvEVMAddress, + [fusdEvEVMAddress, pyusd0EVMAddress, wflowEVMAddress], + [100 as UInt32, 3000 as UInt32] + ], + [adminAccount] + ) + Test.expect(result, Test.beSucceeded()) + + // No WFLOW/WBTC pool on Flow EVM — PYUSD0 is the intermediate for both legs. + log("Configuring FUSDEVStrategy + WBTC (FUSDEV→PYUSD0→WBTC fees 100/3000)...") + result = _executeTransactionFile( + "../transactions/flow-yield-vaults/admin/upsert_strategy_config.cdc", + [ + fusdEvStrategyIdentifier, + wbtcVaultIdentifier, + fusdEvEVMAddress, + [fusdEvEVMAddress, pyusd0EVMAddress, wbtcEVMAddress], + [100 as UInt32, 3000 as UInt32] + ], + [adminAccount] + ) + Test.expect(result, Test.beSucceeded()) + + log("Configuring FUSDEVStrategy + WETH (FUSDEV→PYUSD0→WETH fees 100/3000)...") + result = _executeTransactionFile( + "../transactions/flow-yield-vaults/admin/upsert_strategy_config.cdc", + [ + fusdEvStrategyIdentifier, + wethVaultIdentifier, + fusdEvEVMAddress, + [fusdEvEVMAddress, pyusd0EVMAddress, wethEVMAddress], + [100 as UInt32, 3000 as UInt32] + ], + [adminAccount] + ) + Test.expect(result, Test.beSucceeded()) + + // Register FUSDEVStrategy in the FlowYieldVaults StrategyFactory + log("Registering FUSDEVStrategy in FlowYieldVaults factory...") + result = _executeTransactionFile( + "../transactions/flow-yield-vaults/admin/add_strategy_composer.cdc", + [fusdEvStrategyIdentifier, composerIdentifier, issuerStoragePath], + [adminAccount] + ) + Test.expect(result, Test.beSucceeded()) + + // Grant beta access to all user accounts + log("Granting beta access to WFLOW/PYUSD0 user...") + result = _executeTransactionFile( + "../transactions/flow-yield-vaults/admin/grant_beta.cdc", + [], + [adminAccount, flowUser] + ) + Test.expect(result, Test.beSucceeded()) + + log("Granting beta access to WBTC/WETH user (0x68da18f20e98a7b6)...") + result = _executeTransactionFile( + "../transactions/flow-yield-vaults/admin/grant_beta.cdc", + [], + [adminAccount, wbtcUser] + ) + Test.expect(result, Test.beSucceeded()) + + // Provision extra FLOW to flowUser so that testDepositToFUSDEVYieldVault_WFLOW has enough balance. + // flowUser starts with ~11 FLOW; the create uses 10.0, leaving ~1 FLOW — not enough for a 5.0 deposit. + log("Provisioning 20.0 FLOW to WFLOW user from FlowToken contract account...") + result = _executeTransactionFile( + "../transactions/flow-token/transfer_flow.cdc", + [flowUser.address, 20.0], + [flowTokenAccount] + ) + Test.expect(result, Test.beSucceeded()) + + // Provision WETH and WBTC for the WBTC/WETH user. + // The COA at 0x000000000000000000000002b87c966bc00bc2c4 holds ~12 WETH on mainnet. + log("Bridging 2 WETH from COA to Cadence and swapping 0.1 WETH → WBTC for WBTC/WETH user...") + + // Bridge 2 WETH (2_000_000_000_000_000_000 at 18 decimals) from COA to Cadence. + let bridgeResult = _executeTransactionFile( + "../../lib/FlowALP/FlowActions/cadence/tests/transactions/bridge/bridge_tokens_from_evm.cdc", + [wethVaultIdentifier, 2000000000000000000 as UInt256], + [wbtcUser] + ) + Test.expect(bridgeResult, Test.beSucceeded()) + + // Swap 0.1 WETH → WBTC via UniV3 WETH/WBTC pool (fee 3000). + let swapResult = _executeTransactionFile( + "transactions/provision_wbtc_from_weth.cdc", + [ + "0xca6d7Bb03334bBf135902e1d919a5feccb461632", // UniV3 factory + "0xeEDC6Ff75e1b10B903D9013c358e446a73d35341", // UniV3 router + "0x370A8DF17742867a44e56223EC20D82092242C85", // UniV3 quoter + wethEVMAddress, + wbtcEVMAddress, + 3000 as UInt32, + 0.1 as UFix64 + ], + [wbtcUser] + ) + Test.expect(swapResult, Test.beSucceeded()) + + log("==== Setup Complete ====") +} + +/* ========================================================= + WFLOW (FlowToken) collateral lifecycle + ========================================================= */ + +access(all) fun testCreateFUSDEVYieldVault_WFLOW() { + log("Creating FUSDEVStrategy yield vault with 10.0 FLOW...") + let result = _executeTransactionFile( + "../transactions/flow-yield-vaults/create_yield_vault.cdc", + [fusdEvStrategyIdentifier, flowVaultIdentifier, 10.0], + [flowUser] + ) + Test.expect(result, Test.beSucceeded()) + + flowVaultID = _latestVaultID(flowUser) + log("Created WFLOW vault ID: ".concat(flowVaultID.toString())) + + let bal = _executeScript("../scripts/flow-yield-vaults/get_yield_vault_balance.cdc", [flowUser.address, flowVaultID]) + Test.expect(bal, Test.beSucceeded()) + let balance = bal.returnValue! as! UFix64? + Test.assert(balance != nil && balance! > 0.0, message: "Expected positive balance after create (WFLOW)") + log("WFLOW vault balance after create: ".concat(balance!.toString())) +} + +access(all) fun testDepositToFUSDEVYieldVault_WFLOW() { + let before = (_executeScript("../scripts/flow-yield-vaults/get_yield_vault_balance.cdc", [flowUser.address, flowVaultID]).returnValue! as! UFix64?) ?? 0.0 + let depositAmount: UFix64 = 5.0 + log("Depositing 5.0 FLOW to vault ".concat(flowVaultID.toString()).concat("...")) + Test.expect( + _executeTransactionFile("../transactions/flow-yield-vaults/deposit_to_yield_vault.cdc", [flowVaultID, depositAmount], [flowUser]), + Test.beSucceeded() + ) + let after = (_executeScript("../scripts/flow-yield-vaults/get_yield_vault_balance.cdc", [flowUser.address, flowVaultID]).returnValue! as! UFix64?)! + Test.assert(equalAmounts(a: after, b: before + depositAmount, tolerance: 0.1), + message: "WFLOW deposit: expected ~".concat((before + depositAmount).toString()).concat(", got ").concat(after.toString())) + log("WFLOW vault balance after deposit: ".concat(after.toString())) +} + +access(all) fun testWithdrawFromFUSDEVYieldVault_WFLOW() { + let before = (_executeScript("../scripts/flow-yield-vaults/get_yield_vault_balance.cdc", [flowUser.address, flowVaultID]).returnValue! as! UFix64?) ?? 0.0 + let withdrawAmount: UFix64 = 3.0 + log("Withdrawing 3.0 FLOW from vault ".concat(flowVaultID.toString()).concat("...")) + Test.expect( + _executeTransactionFile("../transactions/flow-yield-vaults/withdraw_from_yield_vault.cdc", [flowVaultID, withdrawAmount], [flowUser]), + Test.beSucceeded() + ) + let after = (_executeScript("../scripts/flow-yield-vaults/get_yield_vault_balance.cdc", [flowUser.address, flowVaultID]).returnValue! as! UFix64?)! + Test.assert(equalAmounts(a: after, b: before - withdrawAmount, tolerance: 0.1), + message: "WFLOW withdraw: expected ~".concat((before - withdrawAmount).toString()).concat(", got ").concat(after.toString())) + log("WFLOW vault balance after withdrawal: ".concat(after.toString())) +} + +access(all) fun testCloseFUSDEVYieldVault_WFLOW() { + let vaultBalBefore = (_executeScript("../scripts/flow-yield-vaults/get_yield_vault_balance.cdc", [flowUser.address, flowVaultID]).returnValue! as! UFix64?) ?? 0.0 + log("Closing WFLOW vault ".concat(flowVaultID.toString()).concat(" (balance: ").concat(vaultBalBefore.toString()).concat(")...")) + Test.expect( + _executeTransactionFile("../transactions/flow-yield-vaults/close_yield_vault.cdc", [flowVaultID], [flowUser]), + Test.beSucceeded() + ) + let vaultBalAfter = _executeScript("../scripts/flow-yield-vaults/get_yield_vault_balance.cdc", [flowUser.address, flowVaultID]) + Test.expect(vaultBalAfter, Test.beSucceeded()) + Test.assert(vaultBalAfter.returnValue == nil, message: "WFLOW vault should no longer exist after close") + log("WFLOW yield vault closed successfully") +} + +/* ========================================================= + WBTC collateral lifecycle + ========================================================= */ + +access(all) fun testCreateFUSDEVYieldVault_WBTC() { + log("Creating FUSDEVStrategy yield vault with 0.0001 WBTC...") + let result = _executeTransactionFile( + "../transactions/flow-yield-vaults/create_yield_vault.cdc", + [fusdEvStrategyIdentifier, wbtcVaultIdentifier, 0.0001], + [wbtcUser] + ) + Test.expect(result, Test.beSucceeded()) + + wbtcVaultID = _latestVaultID(wbtcUser) + log("Created WBTC vault ID: ".concat(wbtcVaultID.toString())) + + let bal = _executeScript("../scripts/flow-yield-vaults/get_yield_vault_balance.cdc", [wbtcUser.address, wbtcVaultID]) + Test.expect(bal, Test.beSucceeded()) + let balance = bal.returnValue! as! UFix64? + Test.assert(balance != nil && balance! > 0.0, message: "Expected positive balance after create (WBTC)") + log("WBTC vault balance after create: ".concat(balance!.toString())) +} + +access(all) fun testDepositToFUSDEVYieldVault_WBTC() { + let before = (_executeScript("../scripts/flow-yield-vaults/get_yield_vault_balance.cdc", [wbtcUser.address, wbtcVaultID]).returnValue! as! UFix64?) ?? 0.0 + let depositAmount: UFix64 = 0.00005 + log("Depositing 0.00005 WBTC to vault ".concat(wbtcVaultID.toString()).concat("...")) + Test.expect( + _executeTransactionFile("../transactions/flow-yield-vaults/deposit_to_yield_vault.cdc", [wbtcVaultID, depositAmount], [wbtcUser]), + Test.beSucceeded() + ) + let after = (_executeScript("../scripts/flow-yield-vaults/get_yield_vault_balance.cdc", [wbtcUser.address, wbtcVaultID]).returnValue! as! UFix64?)! + Test.assert(equalAmounts(a: after, b: before + depositAmount, tolerance: 0.000005), + message: "WBTC deposit: expected ~".concat((before + depositAmount).toString()).concat(", got ").concat(after.toString())) + log("WBTC vault balance after deposit: ".concat(after.toString())) +} + +access(all) fun testWithdrawFromFUSDEVYieldVault_WBTC() { + let before = (_executeScript("../scripts/flow-yield-vaults/get_yield_vault_balance.cdc", [wbtcUser.address, wbtcVaultID]).returnValue! as! UFix64?) ?? 0.0 + let withdrawAmount: UFix64 = 0.00003 + log("Withdrawing 0.00003 WBTC from vault ".concat(wbtcVaultID.toString()).concat("...")) + Test.expect( + _executeTransactionFile("../transactions/flow-yield-vaults/withdraw_from_yield_vault.cdc", [wbtcVaultID, withdrawAmount], [wbtcUser]), + Test.beSucceeded() + ) + let after = (_executeScript("../scripts/flow-yield-vaults/get_yield_vault_balance.cdc", [wbtcUser.address, wbtcVaultID]).returnValue! as! UFix64?)! + Test.assert(equalAmounts(a: after, b: before - withdrawAmount, tolerance: 0.000005), + message: "WBTC withdraw: expected ~".concat((before - withdrawAmount).toString()).concat(", got ").concat(after.toString())) + log("WBTC vault balance after withdrawal: ".concat(after.toString())) +} + +access(all) fun testCloseFUSDEVYieldVault_WBTC() { + let vaultBalBefore = (_executeScript("../scripts/flow-yield-vaults/get_yield_vault_balance.cdc", [wbtcUser.address, wbtcVaultID]).returnValue! as! UFix64?) ?? 0.0 + log("Closing WBTC vault ".concat(wbtcVaultID.toString()).concat(" (balance: ").concat(vaultBalBefore.toString()).concat(")...")) + Test.expect( + _executeTransactionFile("../transactions/flow-yield-vaults/close_yield_vault.cdc", [wbtcVaultID], [wbtcUser]), + Test.beSucceeded() + ) + let vaultBalAfter = _executeScript("../scripts/flow-yield-vaults/get_yield_vault_balance.cdc", [wbtcUser.address, wbtcVaultID]) + Test.expect(vaultBalAfter, Test.beSucceeded()) + Test.assert(vaultBalAfter.returnValue == nil, message: "WBTC vault should no longer exist after close") + log("WBTC yield vault closed successfully") +} + +/* ========================================================= + WETH collateral lifecycle + ========================================================= */ + +access(all) fun testCreateFUSDEVYieldVault_WETH() { + log("Creating FUSDEVStrategy yield vault with 0.001 WETH...") + let result = _executeTransactionFile( + "../transactions/flow-yield-vaults/create_yield_vault.cdc", + [fusdEvStrategyIdentifier, wethVaultIdentifier, 0.001], + [wethUser] + ) + Test.expect(result, Test.beSucceeded()) + + wethVaultID = _latestVaultID(wethUser) + log("Created WETH vault ID: ".concat(wethVaultID.toString())) + + let bal = _executeScript("../scripts/flow-yield-vaults/get_yield_vault_balance.cdc", [wethUser.address, wethVaultID]) + Test.expect(bal, Test.beSucceeded()) + let balance = bal.returnValue! as! UFix64? + Test.assert(balance != nil && balance! > 0.0, message: "Expected positive balance after create (WETH)") + log("WETH vault balance after create: ".concat(balance!.toString())) +} + +access(all) fun testDepositToFUSDEVYieldVault_WETH() { + let before = (_executeScript("../scripts/flow-yield-vaults/get_yield_vault_balance.cdc", [wethUser.address, wethVaultID]).returnValue! as! UFix64?) ?? 0.0 + let depositAmount: UFix64 = 0.0005 + log("Depositing 0.0005 WETH to vault ".concat(wethVaultID.toString()).concat("...")) + Test.expect( + _executeTransactionFile("../transactions/flow-yield-vaults/deposit_to_yield_vault.cdc", [wethVaultID, depositAmount], [wethUser]), + Test.beSucceeded() + ) + let after = (_executeScript("../scripts/flow-yield-vaults/get_yield_vault_balance.cdc", [wethUser.address, wethVaultID]).returnValue! as! UFix64?)! + Test.assert(equalAmounts(a: after, b: before + depositAmount, tolerance: 0.00005), + message: "WETH deposit: expected ~".concat((before + depositAmount).toString()).concat(", got ").concat(after.toString())) + log("WETH vault balance after deposit: ".concat(after.toString())) +} + +access(all) fun testWithdrawFromFUSDEVYieldVault_WETH() { + let before = (_executeScript("../scripts/flow-yield-vaults/get_yield_vault_balance.cdc", [wethUser.address, wethVaultID]).returnValue! as! UFix64?) ?? 0.0 + let withdrawAmount: UFix64 = 0.0003 + log("Withdrawing 0.0003 WETH from vault ".concat(wethVaultID.toString()).concat("...")) + Test.expect( + _executeTransactionFile("../transactions/flow-yield-vaults/withdraw_from_yield_vault.cdc", [wethVaultID, withdrawAmount], [wethUser]), + Test.beSucceeded() + ) + let after = (_executeScript("../scripts/flow-yield-vaults/get_yield_vault_balance.cdc", [wethUser.address, wethVaultID]).returnValue! as! UFix64?)! + Test.assert(equalAmounts(a: after, b: before - withdrawAmount, tolerance: 0.00005), + message: "WETH withdraw: expected ~".concat((before - withdrawAmount).toString()).concat(", got ").concat(after.toString())) + log("WETH vault balance after withdrawal: ".concat(after.toString())) +} + +access(all) fun testCloseFUSDEVYieldVault_WETH() { + let vaultBalBefore = (_executeScript("../scripts/flow-yield-vaults/get_yield_vault_balance.cdc", [wethUser.address, wethVaultID]).returnValue! as! UFix64?) ?? 0.0 + log("Closing WETH vault ".concat(wethVaultID.toString()).concat(" (balance: ").concat(vaultBalBefore.toString()).concat(")...")) + Test.expect( + _executeTransactionFile("../transactions/flow-yield-vaults/close_yield_vault.cdc", [wethVaultID], [wethUser]), + Test.beSucceeded() + ) + let vaultBalAfter = _executeScript("../scripts/flow-yield-vaults/get_yield_vault_balance.cdc", [wethUser.address, wethVaultID]) + Test.expect(vaultBalAfter, Test.beSucceeded()) + Test.assert(vaultBalAfter.returnValue == nil, message: "WETH vault should no longer exist after close") + log("WETH yield vault closed successfully") +} + +/* ========================================================= + Negative tests + ========================================================= */ + +/// PYUSD0 is the underlying asset of FUSDEV — the strategy composer has no collateral config for +/// it, so attempting to create a vault with PYUSD0 as collateral must be rejected. +access(all) fun testCannotCreateYieldVaultWithPYUSD0AsCollateral() { + log("Attempting to create FUSDEVStrategy vault with PYUSD0 (underlying asset) as collateral — expecting failure...") + let result = _executeTransactionFile( + "../transactions/flow-yield-vaults/create_yield_vault.cdc", + [fusdEvStrategyIdentifier, pyusd0VaultIdentifier, 1.0], + [flowUser] + ) + Test.expect(result, Test.beFailed()) + log("Correctly rejected PYUSD0 as collateral") +} + +/// Depositing the wrong token type into an existing YieldVault must be rejected. +/// Here wethUser owns both WETH and WBTC (set up in setup()). +/// We create a fresh WETH vault, then attempt to deposit WBTC into it — the strategy +/// pre-condition should panic on the type mismatch. +access(all) fun testCannotDepositWrongTokenToYieldVault() { + log("Creating a fresh WETH vault for wrong-token deposit test...") + let createResult = _executeTransactionFile( + "../transactions/flow-yield-vaults/create_yield_vault.cdc", + [fusdEvStrategyIdentifier, wethVaultIdentifier, 0.001], + [wethUser] + ) + Test.expect(createResult, Test.beSucceeded()) + let freshWethVaultID = _latestVaultID(wethUser) + log("Created WETH vault ID: ".concat(freshWethVaultID.toString()).concat(" — now attempting to deposit WBTC into it...")) + + // Attempt to deposit WBTC (wrong type) into the WETH vault — must fail + let depositResult = _executeTransactionFile( + "transactions/deposit_wrong_token.cdc", + [freshWethVaultID, wbtcVaultIdentifier, 0.00001], + [wethUser] + ) + Test.expect(depositResult, Test.beFailed()) + log("Correctly rejected wrong-token deposit (WBTC into WETH vault)") +} From 05575b68c33bcbc03194b56cb861f762905151a6 Mon Sep 17 00:00:00 2001 From: Alex <12097569+nialexsan@users.noreply.github.com> Date: Mon, 16 Mar 2026 11:11:36 -0400 Subject: [PATCH 04/29] isolated FUSDEV test --- ...lowYieldVaultsStrategiesV2_FUSDEV_test.cdc | 25 ++++++++++--------- 1 file changed, 13 insertions(+), 12 deletions(-) diff --git a/cadence/tests/FlowYieldVaultsStrategiesV2_FUSDEV_test.cdc b/cadence/tests/FlowYieldVaultsStrategiesV2_FUSDEV_test.cdc index 34c2c202..404b6113 100644 --- a/cadence/tests/FlowYieldVaultsStrategiesV2_FUSDEV_test.cdc +++ b/cadence/tests/FlowYieldVaultsStrategiesV2_FUSDEV_test.cdc @@ -201,24 +201,25 @@ access(all) fun setup() { ) Test.expect(err, Test.beNil()) - log("Deploying FlowYieldVaultsStrategiesV2...") - err = Test.deployContract( - name: "FlowYieldVaultsStrategiesV2", - path: "../../cadence/contracts/FlowYieldVaultsStrategiesV2.cdc", - arguments: [ - "0xca6d7Bb03334bBf135902e1d919a5feccb461632", - "0xeEDC6Ff75e1b10B903D9013c358e446a73d35341", - "0x370A8DF17742867a44e56223EC20D82092242C85" - ] - ) - Test.expect(err, Test.beNil()) + // temporary commented until merged with syWFLOW strategy + // log("Deploying FlowYieldVaultsStrategiesV2...") + // err = Test.deployContract( + // name: "FlowYieldVaultsStrategiesV2", + // path: "../../cadence/contracts/FlowYieldVaultsStrategiesV2.cdc", + // arguments: [ + // "0xca6d7Bb03334bBf135902e1d919a5feccb461632", + // "0xeEDC6Ff75e1b10B903D9013c358e446a73d35341", + // "0x370A8DF17742867a44e56223EC20D82092242C85" + // ] + // ) + // Test.expect(err, Test.beNil()) // Configure UniV3 paths for FUSDEVStrategy. // Closing direction: FUSDEV → PYUSD0 (Morpho redeem, fee 100) → (UniV3 swap, fee 3000). // PYUSD0 is intentionally NOT configured as collateral — it is the underlying asset. log("Configuring FUSDEVStrategy + WFLOW (FUSDEV→PYUSD0→WFLOW fees 100/3000)...") - result = _executeTransactionFile( + var result = _executeTransactionFile( "../transactions/flow-yield-vaults/admin/upsert_strategy_config.cdc", [ fusdEvStrategyIdentifier, From 4aa9ad4b0ad88ebf8c154c2a6f8ded1a373f71d6 Mon Sep 17 00:00:00 2001 From: Alex <12097569+nialexsan@users.noreply.github.com> Date: Mon, 16 Mar 2026 11:15:18 -0400 Subject: [PATCH 05/29] upsert strategy changes from fyv-v2-strategy-deployment --- .../flow-yield-vaults/admin/upsert_strategy_config.cdc | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/cadence/transactions/flow-yield-vaults/admin/upsert_strategy_config.cdc b/cadence/transactions/flow-yield-vaults/admin/upsert_strategy_config.cdc index 792c7031..d36da60b 100644 --- a/cadence/transactions/flow-yield-vaults/admin/upsert_strategy_config.cdc +++ b/cadence/transactions/flow-yield-vaults/admin/upsert_strategy_config.cdc @@ -45,16 +45,13 @@ transaction( return out } - let composerType = Type<@FlowYieldVaultsStrategiesV2.MorphoERC4626StrategyComposer>() - if swapPath.length > 0 { - issuer.addOrUpdateCollateralConfig( - composer: composerType, + issuer.addOrUpdateMorphoCollateralConfig( strategyType: strategyType, collateralVaultType: tokenType, yieldTokenEVMAddress: yieldEVM, yieldToCollateralAddressPath: toEVM(swapPath), - yieldToCollateralFeePath: fees + yieldToCollateralFeePath: fees ) } } From c2cdcb1da4ce096ff6aaf93d8853896a01576b8c Mon Sep 17 00:00:00 2001 From: Alex <12097569+nialexsan@users.noreply.github.com> Date: Mon, 16 Mar 2026 11:17:54 -0400 Subject: [PATCH 06/29] fix typo --- cadence/contracts/FlowYieldVaultsStrategiesV2.cdc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cadence/contracts/FlowYieldVaultsStrategiesV2.cdc b/cadence/contracts/FlowYieldVaultsStrategiesV2.cdc index f0fdbfc7..ec1318cd 100644 --- a/cadence/contracts/FlowYieldVaultsStrategiesV2.cdc +++ b/cadence/contracts/FlowYieldVaultsStrategiesV2.cdc @@ -1171,7 +1171,7 @@ access(all) contract FlowYieldVaultsStrategiesV2 { access(all) view fun getSupportedComposers(): {Type: Bool} { return { - Type<@MorphoERC4626StrategyComposer>(): true, + Type<@MorphoERC4626StrategyComposer>(): true } } From a55ad1cf91acb428477b832c595fb8ae300aa21f Mon Sep 17 00:00:00 2001 From: Alex <12097569+nialexsan@users.noreply.github.com> Date: Mon, 16 Mar 2026 11:31:31 -0400 Subject: [PATCH 07/29] provision script from fyv-v2-strategy-deployment --- .../transactions/provision_wbtc_from_weth.cdc | 96 +++++++++++++++++++ 1 file changed, 96 insertions(+) create mode 100644 cadence/tests/transactions/provision_wbtc_from_weth.cdc diff --git a/cadence/tests/transactions/provision_wbtc_from_weth.cdc b/cadence/tests/transactions/provision_wbtc_from_weth.cdc new file mode 100644 index 00000000..bc7bedc3 --- /dev/null +++ b/cadence/tests/transactions/provision_wbtc_from_weth.cdc @@ -0,0 +1,96 @@ +import "FungibleToken" +import "FungibleTokenMetadataViews" +import "EVM" +import "UniswapV3SwapConnectors" +import "FlowEVMBridgeConfig" + +/// Swap WETH Cadence tokens → WBTC Cadence tokens via the UniV3 WETH/WBTC pool. +/// Sets up the WBTC Cadence vault in signer's storage if not present. +/// +/// @param factoryAddr: UniswapV3 factory EVM address (hex, no 0x prefix) +/// @param routerAddr: UniswapV3 router EVM address +/// @param quoterAddr: UniswapV3 quoter EVM address +/// @param wethEvmAddr: WETH EVM contract address +/// @param wbtcEvmAddr: WBTC (cbBTC) EVM contract address +/// @param fee: UniV3 pool fee tier (e.g. 3000) +/// @param wethAmount: Amount of WETH (Cadence UFix64) to swap for WBTC +/// +transaction( + factoryAddr: String, + routerAddr: String, + quoterAddr: String, + wethEvmAddr: String, + wbtcEvmAddr: String, + fee: UInt32, + wethAmount: UFix64 +) { + prepare(signer: auth(Storage, BorrowValue, IssueStorageCapabilityController, SaveValue, PublishCapability, UnpublishCapability) &Account) { + let coaCap = signer.capabilities.storage.issue(/storage/evm) + + let wethEVM = EVM.addressFromString(wethEvmAddr) + let wbtcEVM = EVM.addressFromString(wbtcEvmAddr) + + let wethType = FlowEVMBridgeConfig.getTypeAssociated(with: wethEVM) + ?? panic("WETH EVM address not registered in bridge config: ".concat(wethEvmAddr)) + let wbtcType = FlowEVMBridgeConfig.getTypeAssociated(with: wbtcEVM) + ?? panic("WBTC EVM address not registered in bridge config: ".concat(wbtcEvmAddr)) + + let swapper = UniswapV3SwapConnectors.Swapper( + factoryAddress: EVM.addressFromString(factoryAddr), + routerAddress: EVM.addressFromString(routerAddr), + quoterAddress: EVM.addressFromString(quoterAddr), + tokenPath: [wethEVM, wbtcEVM], + feePath: [fee], + inVault: wethType, + outVault: wbtcType, + coaCapability: coaCap, + uniqueID: nil + ) + + // Locate WETH vault via FTVaultData so we don't hard-code the storage path. + let wethVaultCompType = CompositeType(wethType.identifier) + ?? panic("Cannot construct CompositeType for WETH: ".concat(wethType.identifier)) + let wethContract = getAccount(wethVaultCompType.address!).contracts.borrow<&{FungibleToken}>(name: wethVaultCompType.contractName!) + ?? panic("Cannot borrow FungibleToken contract for WETH") + let wethVaultData = wethContract.resolveContractView( + resourceType: wethVaultCompType, + viewType: Type() + ) as? FungibleTokenMetadataViews.FTVaultData + ?? panic("Cannot resolve FTVaultData for WETH") + + let wethProvider = signer.storage.borrow( + from: wethVaultData.storagePath + ) ?? panic("No WETH vault in signer's storage at ".concat(wethVaultData.storagePath.toString())) + + let inVault <- wethProvider.withdraw(amount: wethAmount) + + // Swap WETH → WBTC (bridges to EVM, swaps, bridges back to Cadence). + let outVault <- swapper.swap(quote: nil, inVault: <-inVault) + log("Provisioned ".concat(outVault.balance.toString()).concat(" WBTC from ".concat(wethAmount.toString()).concat(" WETH"))) + + // Set up WBTC vault in signer's storage if missing. + let wbtcVaultCompType = CompositeType(wbtcType.identifier) + ?? panic("Cannot construct CompositeType for WBTC: ".concat(wbtcType.identifier)) + let wbtcContract = getAccount(wbtcVaultCompType.address!).contracts.borrow<&{FungibleToken}>(name: wbtcVaultCompType.contractName!) + ?? panic("Cannot borrow FungibleToken contract for WBTC") + let wbtcVaultData = wbtcContract.resolveContractView( + resourceType: wbtcVaultCompType, + viewType: Type() + ) as? FungibleTokenMetadataViews.FTVaultData + ?? panic("Cannot resolve FTVaultData for WBTC") + + if signer.storage.borrow<&{FungibleToken.Vault}>(from: wbtcVaultData.storagePath) == nil { + signer.storage.save(<-wbtcVaultData.createEmptyVault(), to: wbtcVaultData.storagePath) + signer.capabilities.unpublish(wbtcVaultData.receiverPath) + signer.capabilities.unpublish(wbtcVaultData.metadataPath) + let receiverCap = signer.capabilities.storage.issue<&{FungibleToken.Vault}>(wbtcVaultData.storagePath) + let metadataCap = signer.capabilities.storage.issue<&{FungibleToken.Vault}>(wbtcVaultData.storagePath) + signer.capabilities.publish(receiverCap, at: wbtcVaultData.receiverPath) + signer.capabilities.publish(metadataCap, at: wbtcVaultData.metadataPath) + } + + let receiver = signer.storage.borrow<&{FungibleToken.Receiver}>(from: wbtcVaultData.storagePath) + ?? panic("Cannot borrow WBTC vault receiver") + receiver.deposit(from: <-outVault) + } +} From c6a0634e0755a9e2879cafc1d87b964877fbe852 Mon Sep 17 00:00:00 2001 From: Alex <12097569+nialexsan@users.noreply.github.com> Date: Mon, 16 Mar 2026 11:44:30 -0400 Subject: [PATCH 08/29] deposit_wrong_token from fyv-v2-strategies-deployment --- .../transactions/deposit_wrong_token.cdc | 48 +++++++++++++++++++ 1 file changed, 48 insertions(+) create mode 100644 cadence/tests/transactions/deposit_wrong_token.cdc diff --git a/cadence/tests/transactions/deposit_wrong_token.cdc b/cadence/tests/transactions/deposit_wrong_token.cdc new file mode 100644 index 00000000..eacbe0a4 --- /dev/null +++ b/cadence/tests/transactions/deposit_wrong_token.cdc @@ -0,0 +1,48 @@ +import "FungibleToken" +import "FungibleTokenMetadataViews" + +import "FlowYieldVaults" +import "FlowYieldVaultsClosedBeta" + +/// Test-only transaction: attempts to deposit a token of the wrong type into an existing YieldVault. +/// The strategy's pre-condition should reject the mismatched vault type and cause this to fail. +/// +/// @param vaultID: The YieldVault to deposit into +/// @param wrongTokenTypeIdentifier: Type identifier of the wrong token to deposit +/// @param amount: Amount to withdraw from the signer's storage and attempt to deposit +/// +transaction(vaultID: UInt64, wrongTokenTypeIdentifier: String, amount: UFix64) { + let manager: &FlowYieldVaults.YieldVaultManager + let depositVault: @{FungibleToken.Vault} + let betaRef: auth(FlowYieldVaultsClosedBeta.Beta) &FlowYieldVaultsClosedBeta.BetaBadge + + prepare(signer: auth(BorrowValue, CopyValue) &Account) { + let betaCap = signer.storage.copy>( + from: FlowYieldVaultsClosedBeta.UserBetaCapStoragePath + ) ?? panic("Signer does not have a BetaBadge") + self.betaRef = betaCap.borrow() ?? panic("BetaBadge capability is invalid") + + self.manager = signer.storage.borrow<&FlowYieldVaults.YieldVaultManager>( + from: FlowYieldVaults.YieldVaultManagerStoragePath + ) ?? panic("Signer does not have a YieldVaultManager") + + let wrongType = CompositeType(wrongTokenTypeIdentifier) + ?? panic("Invalid type identifier \(wrongTokenTypeIdentifier)") + let tokenContract = getAccount(wrongType.address!).contracts.borrow<&{FungibleToken}>(name: wrongType.contractName!) + ?? panic("Type \(wrongTokenTypeIdentifier) is not a FungibleToken contract") + let vaultData = tokenContract.resolveContractView( + resourceType: wrongType, + viewType: Type() + ) as? FungibleTokenMetadataViews.FTVaultData + ?? panic("No FTVaultData for type \(wrongTokenTypeIdentifier)") + let sourceVault = signer.storage.borrow( + from: vaultData.storagePath + ) ?? panic("Signer has no vault of type \(wrongTokenTypeIdentifier) at path \(vaultData.storagePath)") + + self.depositVault <- sourceVault.withdraw(amount: amount) + } + + execute { + self.manager.depositToYieldVault(betaRef: self.betaRef, vaultID, from: <-self.depositVault) + } +} From c661c96a155aa4745462fca4a153ec8b0e68026a Mon Sep 17 00:00:00 2001 From: Alex <12097569+nialexsan@users.noreply.github.com> Date: Mon, 16 Mar 2026 12:14:14 -0400 Subject: [PATCH 09/29] add missing clean up and remove unused preswap --- .../contracts/FlowYieldVaultsStrategiesV2.cdc | 218 ++---------------- 1 file changed, 14 insertions(+), 204 deletions(-) diff --git a/cadence/contracts/FlowYieldVaultsStrategiesV2.cdc b/cadence/contracts/FlowYieldVaultsStrategiesV2.cdc index ec1318cd..a19a1319 100644 --- a/cadence/contracts/FlowYieldVaultsStrategiesV2.cdc +++ b/cadence/contracts/FlowYieldVaultsStrategiesV2.cdc @@ -49,9 +49,7 @@ access(all) contract FlowYieldVaultsStrategiesV2 { /// "debtToCollateralSwappers" → {UInt64: {DeFiActions.Swapper}} /// "collateralToDebtSwappers" → {UInt64: {DeFiActions.Swapper}} /// "closedPositions" → {UInt64: Bool} - /// "moetPreswapConfigs" → {Type: {Type: MoetPreswapConfig}} /// "originalCollateralTypes" → {UInt64: Type} - /// "collateralPreSwappers" → {UInt64: {DeFiActions.Swapper}} /// "moetToCollateralSwappers" → {UInt64: {DeFiActions.Swapper}} access(contract) let config: {String: AnyStruct} @@ -150,35 +148,6 @@ access(all) contract FlowYieldVaultsStrategiesV2 { } } - /// Configuration for pre-swapping a stablecoin collateral to MOET before depositing into - /// FlowALP. Required when the collateral type is not directly supported by FlowALP (e.g. - /// PYUSD0 must be swapped to MOET since FlowALP only supports MOET as its stablecoin). - /// - /// The path is collateral → MOET (e.g. [PYUSD0_addr, MOET_addr] for a 1-hop swap, or - /// [PYUSD0_addr, WFLOW_addr, MOET_addr] for a 2-hop swap). The reverse (MOET→collateral) - /// is derived automatically by reversing both arrays. - access(all) struct MoetPreswapConfig { - /// Full UniV3 swap path: collateral EVM address → ... → MOET EVM address. - /// First element is the collateral, last element must be MOET. - access(all) let collateralToMoetAddressPath: [EVM.EVMAddress] - /// UniV3 fee tiers for each hop (length must equal addressPath.length - 1). - access(all) let collateralToMoetFeePath: [UInt32] - - init( - collateralToMoetAddressPath: [EVM.EVMAddress], - collateralToMoetFeePath: [UInt32] - ) { - pre { - collateralToMoetAddressPath.length > 1: - "MoetPreswapConfig: path must have at least 2 elements (collateral + MOET)" - collateralToMoetFeePath.length == collateralToMoetAddressPath.length - 1: - "MoetPreswapConfig: fee path length must equal address path length - 1" - } - self.collateralToMoetAddressPath = collateralToMoetAddressPath - self.collateralToMoetFeePath = collateralToMoetFeePath - } - } - /// This strategy uses FUSDEV vault (Morpho ERC4626). /// Deposits collateral into a single FlowALP position, borrowing MOET as debt. /// MOET is swapped to PYUSD0 and deposited into the Morpho FUSDEV ERC4626 vault. @@ -239,27 +208,6 @@ access(all) contract FlowYieldVaultsStrategiesV2 { || (self.uniqueID != nil && FlowYieldVaultsStrategiesV2._getOriginalCollateralType(self.uniqueID!.id) == from.getType()): "FUSDEVStrategy position only accepts \(self.sink.getSinkType().identifier) as collateral, got \(from.getType().identifier)" } - // If depositing the original stablecoin collateral (e.g. PYUSD0), pre-swap to MOET - if let id = self.uniqueID { - if from.getType() != self.sink.getSinkType() { - if let preSwapper = FlowYieldVaultsStrategiesV2._getCollateralPreSwapper(id.id) { - let incoming <- from.withdraw(amount: from.balance) - if incoming.balance > 0.0 { - let quote = preSwapper.quoteOut(forProvided: incoming.balance, reverse: false) - if quote.outAmount > 0.0 { - let moetVault <- preSwapper.swap(quote: quote, inVault: <-incoming) - self.sink.depositCapacity(from: &moetVault as auth(FungibleToken.Withdraw) &{FungibleToken.Vault}) - Burner.burn(<-moetVault) - } else { - Burner.burn(<-incoming) - } - } else { - Burner.burn(<-incoming) - } - return - } - } - } self.sink.depositCapacity(from: from) } /// Withdraws up to the max amount, returning the withdrawn Vault. If the requested token type is unsupported, @@ -462,9 +410,10 @@ access(all) contract FlowYieldVaultsStrategiesV2 { // Clean up stablecoin pre-swap config entries (no-op if not set) if let id = self.uniqueID { FlowYieldVaultsStrategiesV2._removeOriginalCollateralType(id.id) - FlowYieldVaultsStrategiesV2._removeCollateralPreSwapper(id.id) FlowYieldVaultsStrategiesV2._removeMoetToCollateralSwapper(id.id) FlowYieldVaultsStrategiesV2._removeDebtToCollateralSwapper(id.id) + FlowYieldVaultsStrategiesV2._removeYieldToMoetSwapper(id.id) + FlowYieldVaultsStrategiesV2._removeCollateralToDebtSwapper(id.id) } } access(all) fun getComponentInfo(): DeFiActions.ComponentInfo { @@ -744,75 +693,6 @@ access(all) contract FlowYieldVaultsStrategiesV2 { // Store yield→MOET swapper for later access during closePosition FlowYieldVaultsStrategiesV2._setYieldToMoetSwapper(uniqueID.id, yieldToDebtSwapper) - // --- Stablecoin pre-swap path (e.g. PYUSD0 → MOET) --- - // When configured, swap collateral to MOET before depositing into FlowALP, since - // FlowALP only supports MOET as its stablecoin collateral (not PYUSD0 etc.). - if let preswapCfg = FlowYieldVaultsStrategiesV2._getMoetPreswapConfig( - composer: Type<@MorphoERC4626StrategyComposer>(), - collateral: collateralType - ) { - let preSwapper = self._createUniV3Swapper( - tokenPath: preswapCfg.collateralToMoetAddressPath, - feePath: preswapCfg.collateralToMoetFeePath, - inVault: collateralType, - outVault: tokens.moetTokenType, - uniqueID: uniqueID - ) - let preSwapQuote = preSwapper.quoteOut(forProvided: withFunds.balance, reverse: false) - let moetFunds <- preSwapper.swap(quote: preSwapQuote, inVault: <-withFunds) - - // Open FlowALPv0 position with MOET as collateral - let position <- FlowYieldVaultsStrategiesV2._openCreditPosition( - funds: <-moetFunds, - issuanceSink: abaSwapSink, - repaymentSource: abaSwapSource - ) - - // AutoBalancer rebalancing via MOET collateral: - // Overflow: sell FUSDEV → MOET → add to position collateral - // Deficit: pull MOET from position → buy FUSDEV - let positionSink = position.createSinkWithOptions(type: tokens.moetTokenType, pushToDrawDownSink: true) - let positionSwapSink = SwapConnectors.SwapSink( - swapper: yieldToDebtSwapper, // FUSDEV → MOET - sink: positionSink, - uniqueID: uniqueID - ) - let positionSource = position.createSourceWithOptions(type: tokens.moetTokenType, pullFromTopUpSource: false) - let positionSwapSource = SwapConnectors.SwapSource( - swapper: debtToYieldSwapper, // MOET → FUSDEV - source: positionSource, - uniqueID: uniqueID - ) - balancerIO.autoBalancer.setSink(positionSwapSink, updateSinkID: true) - balancerIO.autoBalancer.setSource(positionSwapSource, updateSourceID: true) - - // Store original collateral type (PYUSD0) and pre-swapper for deposit/close - FlowYieldVaultsStrategiesV2._setOriginalCollateralType(uniqueID.id, collateralType) - FlowYieldVaultsStrategiesV2._setCollateralPreSwapper(uniqueID.id, preSwapper) - - // MOET → collateral (e.g. PYUSD0): use the preswap path in reverse. - // Per design: "use the same swapper in reverse during close position". - let moetToOrigCollateral = self._createUniV3Swapper( - tokenPath: preswapCfg.collateralToMoetAddressPath.reverse(), - feePath: preswapCfg.collateralToMoetFeePath.reverse(), - inVault: tokens.moetTokenType, - outVault: collateralType, - uniqueID: uniqueID - ) - // Store under both partitions: moetToCollateralSwappers (for the no-debt close - // path) and debtToCollateralSwappers (for the regular close path MOET dust). - FlowYieldVaultsStrategiesV2._setMoetToCollateralSwapper(uniqueID.id, moetToOrigCollateral) - FlowYieldVaultsStrategiesV2._setDebtToCollateralSwapper(uniqueID.id, moetToOrigCollateral) - // Note: _setCollateralToDebtSwapper is NOT set for stablecoin (MOET) collateral. - // The MOET-direct pre-supplement path in closePosition handles this case. - - return <-create FUSDEVStrategy( - id: uniqueID, - collateralType: tokens.moetTokenType, - position: <-position - ) - } - // --- Standard path (WBTC, WETH — directly supported by FlowALP) --- // Open FlowALPv0 position @@ -1240,45 +1120,12 @@ access(all) contract FlowYieldVaultsStrategiesV2 { self.upsertMorphoConfig(config: { strategyType: { collateralVaultType: base } }) } - /// Configures a stablecoin collateral type to use MOET pre-swap before depositing into - /// FlowALP. Required for stablecoins like PYUSD0 that FlowALP does not support directly - /// as collateral (it only accepts MOET as its stablecoin). - /// - /// collateralToMoetAddressPath: full UniV3 path from collateral EVM address → MOET EVM address - /// collateralToMoetFeePath: UniV3 fee tiers for each hop (length = path length - 1) - access(Configure) fun upsertMoetPreswapConfig( - composer: Type, - collateralVaultType: Type, - collateralToMoetAddressPath: [EVM.EVMAddress], - collateralToMoetFeePath: [UInt32] - ) { - pre { - composer == Type<@MorphoERC4626StrategyComposer>(): - "composer must be MorphoERC4626StrategyComposer" - collateralVaultType.isSubtype(of: Type<@{FungibleToken.Vault}>()): - "collateralVaultType must be a FungibleToken.Vault" - collateralToMoetAddressPath.length > 1: - "Path must have at least 2 elements" - collateralToMoetFeePath.length == collateralToMoetAddressPath.length - 1: - "Fee path length must equal address path length - 1" - } - FlowYieldVaultsStrategiesV2._setMoetPreswapConfig( - composer: composer, - collateral: collateralVaultType, - cfg: FlowYieldVaultsStrategiesV2.MoetPreswapConfig( - collateralToMoetAddressPath: collateralToMoetAddressPath, - collateralToMoetFeePath: collateralToMoetFeePath - ) - ) - } - access(Configure) fun purgeConfig() { self.configs = { Type<@MorphoERC4626StrategyComposer>(): { Type<@FUSDEVStrategy>(): {} as {Type: CollateralConfig} } } - FlowYieldVaultsStrategiesV2.config["moetPreswapConfigs"] = {} as {Type: {Type: FlowYieldVaultsStrategiesV2.MoetPreswapConfig}} } } @@ -1357,6 +1204,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}? { @@ -1392,6 +1245,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 { @@ -1437,30 +1296,8 @@ access(all) contract FlowYieldVaultsStrategiesV2 { FlowYieldVaultsStrategiesV2.config["originalCollateralTypes"] = partition } - // --- "collateralPreSwappers" partition --- - // Stores a collateral→MOET swapper per strategy uniqueID. - // Used in deposit() to pre-swap incoming stablecoin collateral (e.g. PYUSD0) to MOET. - - access(contract) view fun _getCollateralPreSwapper(_ id: UInt64): {DeFiActions.Swapper}? { - let partition = FlowYieldVaultsStrategiesV2.config["collateralPreSwappers"] as! {UInt64: {DeFiActions.Swapper}}? ?? {} - return partition[id] - } - - access(contract) fun _setCollateralPreSwapper(_ id: UInt64, _ swapper: {DeFiActions.Swapper}) { - var partition = FlowYieldVaultsStrategiesV2.config["collateralPreSwappers"] as! {UInt64: {DeFiActions.Swapper}}? ?? {} - partition[id] = swapper - FlowYieldVaultsStrategiesV2.config["collateralPreSwappers"] = partition - } - - access(contract) fun _removeCollateralPreSwapper(_ id: UInt64) { - var partition = FlowYieldVaultsStrategiesV2.config["collateralPreSwappers"] as! {UInt64: {DeFiActions.Swapper}}? ?? {} - partition.remove(key: id) - FlowYieldVaultsStrategiesV2.config["collateralPreSwappers"] = partition - } - // --- "moetToCollateralSwappers" partition --- // Stores a MOET→original-collateral swapper per strategy uniqueID (FUSDEVStrategy). - // Built from the reversed MoetPreswapConfig path (same path in reverse). // Used in closePosition to convert returned MOET collateral back to the original stablecoin // (e.g. PYUSD0) for the no-debt path. The regular close path uses debtToCollateralSwappers. @@ -1481,33 +1318,6 @@ access(all) contract FlowYieldVaultsStrategiesV2 { FlowYieldVaultsStrategiesV2.config["moetToCollateralSwappers"] = partition } - // --- "moetPreswapConfigs" partition --- - // Static admin config: keyed by composerType → collateralType → MoetPreswapConfig. - // Checked during createStrategy to determine whether a collateral needs MOET pre-swap. - - access(contract) view fun _getMoetPreswapConfig( - composer: Type, - collateral: Type - ): MoetPreswapConfig? { - let partition = FlowYieldVaultsStrategiesV2.config["moetPreswapConfigs"] - as! {Type: {Type: MoetPreswapConfig}}? ?? {} - let p = partition[composer] ?? {} - return p[collateral] - } - - access(contract) fun _setMoetPreswapConfig( - composer: Type, - collateral: Type, - cfg: MoetPreswapConfig - ) { - var partition = FlowYieldVaultsStrategiesV2.config["moetPreswapConfigs"] - as! {Type: {Type: MoetPreswapConfig}}? ?? {} - var p = partition[composer] ?? {} - p[collateral] = cfg - partition[composer] = p - FlowYieldVaultsStrategiesV2.config["moetPreswapConfigs"] = partition - } - init( univ3FactoryEVMAddress: String, univ3RouterEVMAddress: String, From 009d413ba72c334cc3f985a993c272c89e6e6e54 Mon Sep 17 00:00:00 2001 From: Alex <12097569+nialexsan@users.noreply.github.com> Date: Mon, 16 Mar 2026 13:11:18 -0400 Subject: [PATCH 10/29] quote fix --- cadence/contracts/FlowYieldVaultsStrategiesV2.cdc | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/cadence/contracts/FlowYieldVaultsStrategiesV2.cdc b/cadence/contracts/FlowYieldVaultsStrategiesV2.cdc index a19a1319..10e0d63f 100644 --- a/cadence/contracts/FlowYieldVaultsStrategiesV2.cdc +++ b/cadence/contracts/FlowYieldVaultsStrategiesV2.cdc @@ -328,7 +328,12 @@ access(all) contract FlowYieldVaultsStrategiesV2 { if quote.inAmount > 0.0 { let extraCollateral <- self.source.withdrawAvailable(maxAmount: quote.inAmount) if extraCollateral.balance > 0.0 { - let extraMOET <- collateralToMoetSwapper.swap(quote: quote, inVault: <-extraCollateral) + // Re-quote against the actual withdrawn amount: if the source was underfunded, + // extraCollateral.balance < quote.inAmount. Passing the stale quote to swap() + // would set amountOutMinimum = buffered (the full desired output), which the + // smaller input cannot achieve, causing the UniV3 exactInput call to revert. + let actualQuote = collateralToMoetSwapper.quoteOut(forProvided: extraCollateral.balance, reverse: false) + let extraMOET <- collateralToMoetSwapper.swap(quote: actualQuote, inVault: <-extraCollateral) if extraMOET.balance > 0.0 { // Deposit MOET to reduce position debt before close self.position.deposit(from: <-extraMOET) From 4d75cc2986d6558f5e4bd3e0abad3b1099fc5775 Mon Sep 17 00:00:00 2001 From: Alex <12097569+nialexsan@users.noreply.github.com> Date: Mon, 16 Mar 2026 14:47:50 -0400 Subject: [PATCH 11/29] remove unused pre-swap logic --- .../contracts/FlowYieldVaultsStrategiesV2.cdc | 105 +----------------- 1 file changed, 4 insertions(+), 101 deletions(-) diff --git a/cadence/contracts/FlowYieldVaultsStrategiesV2.cdc b/cadence/contracts/FlowYieldVaultsStrategiesV2.cdc index 10e0d63f..ee30b014 100644 --- a/cadence/contracts/FlowYieldVaultsStrategiesV2.cdc +++ b/cadence/contracts/FlowYieldVaultsStrategiesV2.cdc @@ -49,8 +49,6 @@ access(all) contract FlowYieldVaultsStrategiesV2 { /// "debtToCollateralSwappers" → {UInt64: {DeFiActions.Swapper}} /// "collateralToDebtSwappers" → {UInt64: {DeFiActions.Swapper}} /// "closedPositions" → {UInt64: Bool} - /// "originalCollateralTypes" → {UInt64: Type} - /// "moetToCollateralSwappers" → {UInt64: {DeFiActions.Swapper}} access(contract) let config: {String: AnyStruct} /// Canonical StoragePath where the StrategyComposerIssuer should be stored @@ -176,36 +174,19 @@ access(all) contract FlowYieldVaultsStrategiesV2 { // access(all) view fun isSupportedCollateralType(_ type: Type): Bool access(all) view fun getSupportedCollateralTypes(): {Type: Bool} { - // If this strategy was initialized with a stablecoin pre-swap (e.g. PYUSD0→MOET), - // expose the original (external) collateral type to callers, not the internal MOET type. - if let id = self.uniqueID { - if let originalType = FlowYieldVaultsStrategiesV2._getOriginalCollateralType(id.id) { - return { originalType: true } - } - } return { self.sink.getSinkType(): true } } /// Returns the amount available for withdrawal via the inner Source access(all) fun availableBalance(ofToken: Type): UFix64 { if FlowYieldVaultsStrategiesV2._isPositionClosed(self.uniqueID) { return 0.0 } - // If stablecoin pre-swap is in effect, match against the original (external) collateral type. - // MOET and PYUSD0 are both stablecoins with approximately equal value (1:1), so the MOET - // balance is a reasonable approximation of the PYUSD0-denominated collateral balance. - var effectiveSourceType = self.source.getSourceType() - if let id = self.uniqueID { - if let originalType = FlowYieldVaultsStrategiesV2._getOriginalCollateralType(id.id) { - effectiveSourceType = originalType - } - } - return ofToken == effectiveSourceType ? self.source.minimumAvailable() : 0.0 + return ofToken == self.source.getSourceType() ? self.source.minimumAvailable() : 0.0 } /// Deposits up to the inner Sink's capacity from the provided authorized Vault reference. - /// Accepts both the internal collateral type (MOET) and, when a pre-swap is configured, - /// the original external collateral type (e.g. PYUSD0) — which is swapped to MOET first. + /// Only accepts the sink's collateral type (e.g. WBTC, WETH). PYUSD0 is not accepted + /// because it is the FUSDEV vault's underlying asset, not a valid collateral token. access(all) fun deposit(from: auth(FungibleToken.Withdraw) &{FungibleToken.Vault}) { pre { - from.getType() == self.sink.getSinkType() - || (self.uniqueID != nil && FlowYieldVaultsStrategiesV2._getOriginalCollateralType(self.uniqueID!.id) == from.getType()): + from.getType() == self.sink.getSinkType(): "FUSDEVStrategy position only accepts \(self.sink.getSinkType().identifier) as collateral, got \(from.getType().identifier)" } self.sink.depositCapacity(from: from) @@ -273,22 +254,6 @@ access(all) contract FlowYieldVaultsStrategiesV2 { } var collateralVault <- resultVaults.removeFirst() destroy resultVaults - // 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 - } - } - Burner.burn(<-collateralVault) - FlowYieldVaultsStrategiesV2._markPositionClosed(self.uniqueID) - return <- DeFiActionsUtils.getEmptyVault(collateralType) - } - } FlowYieldVaultsStrategiesV2._markPositionClosed(self.uniqueID) return <- collateralVault } @@ -344,22 +309,6 @@ access(all) contract FlowYieldVaultsStrategiesV2 { Burner.burn(<-extraCollateral) } } - } else { - if let id = self.uniqueID { - if FlowYieldVaultsStrategiesV2._getOriginalCollateralType(id.id) != nil { - // Stablecoin pre-swap case: position collateral IS MOET (same as debt token). - // Pull MOET directly from the collateral source to pre-reduce debt — no swap needed. - let shortfall = totalDebtAmount - expectedMOET - let buffered = shortfall + shortfall / 100.0 - let extraMOET <- self.source.withdrawAvailable(maxAmount: buffered) - if extraMOET.balance > 0.0 { - self.position.deposit(from: <-extraMOET) - } else { - Burner.burn(<-extraMOET) - } - } - } - } } // Step 7: Create a BufferedSwapSource that converts ALL yield tokens → MOET. @@ -412,10 +361,7 @@ access(all) contract FlowYieldVaultsStrategiesV2 { access(contract) fun burnCallback() { FlowYieldVaultsAutoBalancers._cleanupAutoBalancer(id: self.id()!) FlowYieldVaultsStrategiesV2._cleanupPositionClosed(self.uniqueID) - // Clean up stablecoin pre-swap config entries (no-op if not set) if let id = self.uniqueID { - FlowYieldVaultsStrategiesV2._removeOriginalCollateralType(id.id) - FlowYieldVaultsStrategiesV2._removeMoetToCollateralSwapper(id.id) FlowYieldVaultsStrategiesV2._removeDebtToCollateralSwapper(id.id) FlowYieldVaultsStrategiesV2._removeYieldToMoetSwapper(id.id) FlowYieldVaultsStrategiesV2._removeCollateralToDebtSwapper(id.id) @@ -1280,49 +1226,6 @@ access(all) contract FlowYieldVaultsStrategiesV2 { } } - // --- "originalCollateralTypes" partition --- - // Stores the original (external) collateral type per strategy uniqueID when a MOET pre-swap - // is in effect. E.g. PYUSD0 when the position internally holds MOET. - - access(contract) view fun _getOriginalCollateralType(_ id: UInt64): Type? { - let partition = FlowYieldVaultsStrategiesV2.config["originalCollateralTypes"] as! {UInt64: Type}? ?? {} - return partition[id] - } - - access(contract) fun _setOriginalCollateralType(_ id: UInt64, _ t: Type) { - var partition = FlowYieldVaultsStrategiesV2.config["originalCollateralTypes"] as! {UInt64: Type}? ?? {} - partition[id] = t - FlowYieldVaultsStrategiesV2.config["originalCollateralTypes"] = partition - } - - access(contract) fun _removeOriginalCollateralType(_ id: UInt64) { - var partition = FlowYieldVaultsStrategiesV2.config["originalCollateralTypes"] as! {UInt64: Type}? ?? {} - partition.remove(key: id) - FlowYieldVaultsStrategiesV2.config["originalCollateralTypes"] = partition - } - - // --- "moetToCollateralSwappers" partition --- - // Stores a MOET→original-collateral swapper per strategy uniqueID (FUSDEVStrategy). - // Used in closePosition to convert returned MOET collateral back to the original stablecoin - // (e.g. PYUSD0) for the no-debt path. The regular close path uses debtToCollateralSwappers. - - access(contract) view fun _getMoetToCollateralSwapper(_ id: UInt64): {DeFiActions.Swapper}? { - let partition = FlowYieldVaultsStrategiesV2.config["moetToCollateralSwappers"] as! {UInt64: {DeFiActions.Swapper}}? ?? {} - return partition[id] - } - - access(contract) fun _setMoetToCollateralSwapper(_ id: UInt64, _ swapper: {DeFiActions.Swapper}) { - var partition = FlowYieldVaultsStrategiesV2.config["moetToCollateralSwappers"] as! {UInt64: {DeFiActions.Swapper}}? ?? {} - partition[id] = swapper - FlowYieldVaultsStrategiesV2.config["moetToCollateralSwappers"] = partition - } - - access(contract) fun _removeMoetToCollateralSwapper(_ id: UInt64) { - var partition = FlowYieldVaultsStrategiesV2.config["moetToCollateralSwappers"] as! {UInt64: {DeFiActions.Swapper}}? ?? {} - partition.remove(key: id) - FlowYieldVaultsStrategiesV2.config["moetToCollateralSwappers"] = partition - } - init( univ3FactoryEVMAddress: String, univ3RouterEVMAddress: String, From 48ef07bd195a8aa3c529064f13553e1200f2dff8 Mon Sep 17 00:00:00 2001 From: Alex <12097569+nialexsan@users.noreply.github.com> Date: Mon, 16 Mar 2026 15:07:50 -0400 Subject: [PATCH 12/29] fix typo --- cadence/contracts/FlowYieldVaultsStrategiesV2.cdc | 1 + 1 file changed, 1 insertion(+) diff --git a/cadence/contracts/FlowYieldVaultsStrategiesV2.cdc b/cadence/contracts/FlowYieldVaultsStrategiesV2.cdc index ee30b014..3dd5adb6 100644 --- a/cadence/contracts/FlowYieldVaultsStrategiesV2.cdc +++ b/cadence/contracts/FlowYieldVaultsStrategiesV2.cdc @@ -309,6 +309,7 @@ access(all) contract FlowYieldVaultsStrategiesV2 { Burner.burn(<-extraCollateral) } } + } } // Step 7: Create a BufferedSwapSource that converts ALL yield tokens → MOET. From 6b0d51afd9f02816a73d7e012b8308fe0b27adf5 Mon Sep 17 00:00:00 2001 From: Alex <12097569+nialexsan@users.noreply.github.com> Date: Tue, 17 Mar 2026 12:57:05 -0400 Subject: [PATCH 13/29] remove fusdev related code --- .../contracts/FlowYieldVaultsStrategiesV2.cdc | 332 ++++++++++++------ .../transactions/provision_wbtc_from_weth.cdc | 2 +- 2 files changed, 225 insertions(+), 109 deletions(-) diff --git a/cadence/contracts/FlowYieldVaultsStrategiesV2.cdc b/cadence/contracts/FlowYieldVaultsStrategiesV2.cdc index 3dd5adb6..0b0f3f80 100644 --- a/cadence/contracts/FlowYieldVaultsStrategiesV2.cdc +++ b/cadence/contracts/FlowYieldVaultsStrategiesV2.cdc @@ -45,9 +45,6 @@ access(all) contract FlowYieldVaultsStrategiesV2 { /// Partitioned config map. Each key is a partition name; each value is a typed nested map keyed by /// strategy UniqueIdentifier ID (UInt64). Current partitions: - /// "yieldToMoetSwappers" → {UInt64: {DeFiActions.Swapper}} - /// "debtToCollateralSwappers" → {UInt64: {DeFiActions.Swapper}} - /// "collateralToDebtSwappers" → {UInt64: {DeFiActions.Swapper}} /// "closedPositions" → {UInt64: Bool} access(contract) let config: {String: AnyStruct} @@ -262,9 +259,26 @@ access(all) contract FlowYieldVaultsStrategiesV2 { let yieldTokenSource = FlowYieldVaultsAutoBalancers.createExternalSource(id: self.id()!) ?? panic("Could not create external source from AutoBalancer") - // Step 5: Retrieve yield→MOET swapper - let yieldToMoetSwapper = FlowYieldVaultsStrategiesV2._getYieldToMoetSwapper(self.uniqueID!.id) - ?? panic("No yield→MOET swapper found for strategy \(self.id()!)") + // Step 5: Reconstruct yield→MOET swapper from stored CollateralConfig. + // The swapper is no longer cached in the config dict (too expensive to write as + // the debtToCollateralSwappers partition grows on mainnet). Reconstruct instead. + // + // For stablecoin path (e.g. PYUSD0 collateral → internal MOET collateral), + // _getOriginalCollateralType gives us the user-facing key for CollateralConfig lookup. + let closeCollateralKey = self.uniqueID != nil + ? FlowYieldVaultsStrategiesV2._getOriginalCollateralType(self.uniqueID!.id) ?? collateralType + : collateralType + let closeCollateralConfig = FlowYieldVaultsStrategiesV2._getStoredCollateralConfig( + strategyType: Type<@FUSDEVStrategy>(), + collateralType: closeCollateralKey + ) ?? panic("No CollateralConfig for FUSDEVStrategy with \(closeCollateralKey.identifier)") + let closeTokens = FlowYieldVaultsStrategiesV2._resolveTokenBundle( + yieldTokenEVMAddress: closeCollateralConfig.yieldTokenEVMAddress + ) + let yieldToMoetSwapper = FlowYieldVaultsStrategiesV2._buildYieldToDebtSwapper( + tokens: closeTokens, + uniqueID: self.uniqueID! + ) // Step 6: Pre-supplement from collateral if yield is insufficient to cover the full debt. // @@ -285,29 +299,32 @@ access(all) contract FlowYieldVaultsStrategiesV2 { ? yieldToMoetSwapper.quoteOut(forProvided: yieldAvail, reverse: false).outAmount : 0.0 if expectedMOET < totalDebtAmount { - if let collateralToMoetSwapper = FlowYieldVaultsStrategiesV2._getCollateralToDebtSwapper(self.uniqueID!.id) { - let shortfall = totalDebtAmount - expectedMOET - // Add 1% buffer to account for swap slippage/rounding in the collateral→MOET leg - let buffered = shortfall + shortfall / 100.0 - let quote = collateralToMoetSwapper.quoteIn(forDesired: buffered, reverse: false) - if quote.inAmount > 0.0 { - let extraCollateral <- self.source.withdrawAvailable(maxAmount: quote.inAmount) - if extraCollateral.balance > 0.0 { - // Re-quote against the actual withdrawn amount: if the source was underfunded, - // extraCollateral.balance < quote.inAmount. Passing the stale quote to swap() - // would set amountOutMinimum = buffered (the full desired output), which the - // smaller input cannot achieve, causing the UniV3 exactInput call to revert. - let actualQuote = collateralToMoetSwapper.quoteOut(forProvided: extraCollateral.balance, reverse: false) - let extraMOET <- collateralToMoetSwapper.swap(quote: actualQuote, inVault: <-extraCollateral) - if extraMOET.balance > 0.0 { - // Deposit MOET to reduce position debt before close - self.position.deposit(from: <-extraMOET) - } else { - Burner.burn(<-extraMOET) - } + let collateralToMoetSwapper = FlowYieldVaultsStrategiesV2._reconstructCollateralToDebtSwapper( + collateralConfig: collateralConfig, + collateralType: collateralType, + uniqueID: self.uniqueID! + ) + let shortfall = totalDebtAmount - expectedMOET + // Add 1% buffer to account for swap slippage/rounding in the collateral→MOET leg + let buffered = shortfall + shortfall / 100.0 + let quote = collateralToMoetSwapper.quoteIn(forDesired: buffered, reverse: false) + if quote.inAmount > 0.0 { + let extraCollateral <- self.source.withdrawAvailable(maxAmount: quote.inAmount) + if extraCollateral.balance > 0.0 { + // Re-quote against the actual withdrawn amount: if the source was underfunded, + // extraCollateral.balance < quote.inAmount. Passing the stale quote to swap() + // would set amountOutMinimum = buffered (the full desired output), which the + // smaller input cannot achieve, causing the UniV3 exactInput call to revert. + let actualQuote = collateralToMoetSwapper.quoteOut(forProvided: extraCollateral.balance, reverse: false) + let extraMOET <- collateralToMoetSwapper.swap(quote: actualQuote, inVault: <-extraCollateral) + if extraMOET.balance > 0.0 { + // Deposit MOET to reduce position debt before close + self.position.deposit(from: <-extraMOET) } else { - Burner.burn(<-extraCollateral) + Burner.burn(<-extraMOET) } + } else { + Burner.burn(<-extraCollateral) } } } @@ -328,8 +345,15 @@ access(all) contract FlowYieldVaultsStrategiesV2 { // 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. - let debtToCollateralSwapper = FlowYieldVaultsStrategiesV2._getDebtToCollateralSwapper(self.uniqueID!.id) + // vaults (MOET overpayment dust) back to collateral via reconstructed swapper. + // + // Standard path: reconstruct MOET→YIELD→collateral from CollateralConfig. + let debtToCollateralSwapper = FlowYieldVaultsStrategiesV2._resolveDebtToCollateralSwapper( + uniqueID: self.uniqueID!, + collateralConfig: closeCollateralConfig, + tokens: closeTokens, + collateralType: collateralType + ) var collateralVault <- DeFiActionsUtils.getEmptyVault(collateralType) while resultVaults.length > 0 { @@ -362,10 +386,9 @@ access(all) contract FlowYieldVaultsStrategiesV2 { access(contract) fun burnCallback() { FlowYieldVaultsAutoBalancers._cleanupAutoBalancer(id: self.id()!) FlowYieldVaultsStrategiesV2._cleanupPositionClosed(self.uniqueID) + // Clean up stablecoin pre-swap config entries (no-op if not set) if let id = self.uniqueID { FlowYieldVaultsStrategiesV2._removeDebtToCollateralSwapper(id.id) - FlowYieldVaultsStrategiesV2._removeYieldToMoetSwapper(id.id) - FlowYieldVaultsStrategiesV2._removeCollateralToDebtSwapper(id.id) } } access(all) fun getComponentInfo(): DeFiActions.ComponentInfo { @@ -641,12 +664,6 @@ access(all) contract FlowYieldVaultsStrategiesV2 { source: balancerIO.source, uniqueID: uniqueID ) - - // Store yield→MOET swapper for later access during closePosition - FlowYieldVaultsStrategiesV2._setYieldToMoetSwapper(uniqueID.id, yieldToDebtSwapper) - - // --- Standard path (WBTC, WETH — directly supported by FlowALP) --- - // Open FlowALPv0 position let position <- FlowYieldVaultsStrategiesV2._openCreditPosition( funds: <-withFunds, @@ -696,26 +713,6 @@ access(all) contract FlowYieldVaultsStrategiesV2 { balancerIO.autoBalancer.setSink(positionSwapSink, updateSinkID: true) balancerIO.autoBalancer.setSource(positionSwapSource, updateSourceID: true) - // Store collateral→MOET swapper for pre-supplement in closePosition. - // Used to cover the ~0.02% round-trip fee shortfall when yield hasn't accrued. - let collateralToDebtSwapper = self._createCollateralToDebtSwapper( - collateralConfig: collateralConfig, - tokens: tokens, - collateralType: collateralType, - uniqueID: uniqueID - ) - FlowYieldVaultsStrategiesV2._setCollateralToDebtSwapper(uniqueID.id, collateralToDebtSwapper) - - // Store MOET→collateral swapper for dust conversion in closePosition. - // Chain: MOET → FUSDEV (debtToYieldSwapper) → collateral (yieldToCollateralSwapper) - FlowYieldVaultsStrategiesV2._setDebtToCollateralSwapper( - uniqueID.id, - SwapConnectors.SequentialSwapper( - swappers: [debtToYieldSwapper, yieldToCollateralSwapper], - uniqueID: uniqueID - ) - ) - return <-create FUSDEVStrategy( id: uniqueID, collateralType: collateralType, @@ -1001,6 +998,19 @@ access(all) contract FlowYieldVaultsStrategiesV2 { return false } + access(all) fun getCollateralConfig( + composerType: Type, + strategyType: Type, + collateralType: Type + ): CollateralConfig? { + if let p0 = self.configs[composerType] { + if let p1 = p0[strategyType] { + return p1[collateralType] + } + } + return nil + } + access(all) view fun getSupportedComposers(): {Type: Bool} { return { Type<@MorphoERC4626StrategyComposer>(): true @@ -1085,9 +1095,14 @@ access(all) contract FlowYieldVaultsStrategiesV2 { /// TODO: this is temporary until we have a better way to pass user's COAs to inner connectors access(self) fun _getCOACapability(): Capability { - let coaCap = self.account.capabilities.storage.issue(/storage/evm) - assert(coaCap.check(), message: "Could not issue COA capability") - return coaCap + let capPath = /storage/strategiesCOACap + if self.account.storage.type(at: capPath) == nil { + let coaCap = self.account.capabilities.storage.issue(/storage/evm) + assert(coaCap.check(), message: "Could not issue COA capability") + self.account.storage.save(coaCap, to: capPath) + } + return self.account.storage.copy>(from: capPath) + ?? panic("Could not load COA capability from storage") } /// Returns a FungibleTokenConnectors.VaultSinkAndSource used to subsidize cross VM token movement in contract- @@ -1143,64 +1158,165 @@ access(all) contract FlowYieldVaultsStrategiesV2 { ) } - // --- "yieldToMoetSwappers" partition --- - - access(contract) view fun _getYieldToMoetSwapper(_ id: UInt64): {DeFiActions.Swapper}? { - let partition = FlowYieldVaultsStrategiesV2.config["yieldToMoetSwappers"] as! {UInt64: {DeFiActions.Swapper}}? ?? {} - return partition[id] - } - - access(contract) fun _setYieldToMoetSwapper(_ id: UInt64, _ swapper: {DeFiActions.Swapper}) { - var partition = FlowYieldVaultsStrategiesV2.config["yieldToMoetSwappers"] as! {UInt64: {DeFiActions.Swapper}}? ?? {} - partition[id] = swapper - 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 + // --- Reconstruction helpers --- + // These allow closePosition to rebuild swappers from stored CollateralConfig at close time, + // avoiding the expensive per-position config dict writes during createStrategy. + + /// Reads CollateralConfig from StrategyComposerIssuer, returning a value copy. + access(self) fun _getStoredCollateralConfig( + strategyType: Type, + collateralType: Type + ): CollateralConfig? { + let issuer = FlowYieldVaultsStrategiesV2.account.storage.borrow< + &FlowYieldVaultsStrategiesV2.StrategyComposerIssuer + >(from: FlowYieldVaultsStrategiesV2.IssuerStoragePath) + if issuer == nil { return nil } + return issuer!.getCollateralConfig(strategyType: strategyType, collateralType: collateralType) } - // --- "debtToCollateralSwappers" partition --- - - access(contract) view fun _getDebtToCollateralSwapper(_ id: UInt64): {DeFiActions.Swapper}? { - let partition = FlowYieldVaultsStrategiesV2.config["debtToCollateralSwappers"] as! {UInt64: {DeFiActions.Swapper}}? ?? {} - return partition[id] - } - - access(contract) fun _setDebtToCollateralSwapper(_ id: UInt64, _ swapper: {DeFiActions.Swapper}) { - var partition = FlowYieldVaultsStrategiesV2.config["debtToCollateralSwappers"] as! {UInt64: {DeFiActions.Swapper}}? ?? {} - partition[id] = swapper - FlowYieldVaultsStrategiesV2.config["debtToCollateralSwappers"] = partition + /// Builds a UniswapV3 swapper. Contract-level equivalent of MorphoERC4626StrategyComposer._createUniV3Swapper. + access(self) fun _buildUniV3Swapper( + tokenPath: [EVM.EVMAddress], + feePath: [UInt32], + inVault: Type, + outVault: Type, + uniqueID: DeFiActions.UniqueIdentifier + ): UniswapV3SwapConnectors.Swapper { + return UniswapV3SwapConnectors.Swapper( + factoryAddress: FlowYieldVaultsStrategiesV2.univ3FactoryEVMAddress, + routerAddress: FlowYieldVaultsStrategiesV2.univ3RouterEVMAddress, + quoterAddress: FlowYieldVaultsStrategiesV2.univ3QuoterEVMAddress, + tokenPath: tokenPath, + feePath: feePath, + inVault: inVault, + outVault: outVault, + coaCapability: FlowYieldVaultsStrategiesV2._getCOACapability(), + uniqueID: uniqueID + ) } - access(contract) fun _removeDebtToCollateralSwapper(_ id: UInt64) { - var partition = FlowYieldVaultsStrategiesV2.config["debtToCollateralSwappers"] as! {UInt64: {DeFiActions.Swapper}}? ?? {} - partition.remove(key: id) - FlowYieldVaultsStrategiesV2.config["debtToCollateralSwappers"] = partition + /// Builds a YIELD→MOET MultiSwapper from a TokenBundle. + /// Contract-level equivalent of MorphoERC4626StrategyComposer._createYieldToDebtSwapper. + access(self) fun _buildYieldToDebtSwapper( + tokens: FlowYieldVaultsStrategiesV2.TokenBundle, + uniqueID: DeFiActions.UniqueIdentifier + ): SwapConnectors.MultiSwapper { + let yieldToDebtAMM = FlowYieldVaultsStrategiesV2._buildUniV3Swapper( + tokenPath: [tokens.yieldTokenEVMAddress, tokens.moetTokenEVMAddress], + feePath: [100], + inVault: tokens.yieldTokenType, + outVault: tokens.moetTokenType, + uniqueID: uniqueID + ) + let yieldToUnderlying = MorphoERC4626SwapConnectors.Swapper( + vaultEVMAddress: tokens.yieldTokenEVMAddress, + coa: FlowYieldVaultsStrategiesV2._getCOACapability(), + feeSource: FlowYieldVaultsStrategiesV2._createFeeSource(withID: uniqueID), + uniqueID: uniqueID, + isReversed: true + ) + let underlyingToDebt = FlowYieldVaultsStrategiesV2._buildUniV3Swapper( + tokenPath: [tokens.underlying4626AssetEVMAddress, tokens.moetTokenEVMAddress], + feePath: [100], + inVault: tokens.underlying4626AssetType, + outVault: tokens.moetTokenType, + uniqueID: uniqueID + ) + let seq = SwapConnectors.SequentialSwapper( + swappers: [yieldToUnderlying, underlyingToDebt], + uniqueID: uniqueID + ) + return SwapConnectors.MultiSwapper( + inVault: tokens.yieldTokenType, + outVault: tokens.moetTokenType, + swappers: [yieldToDebtAMM, seq], + uniqueID: uniqueID + ) } - // --- "collateralToDebtSwappers" partition --- - // Stores a collateral→debt (collateral→PYUSD0→MOET) UniV3 swapper per FUSDEVStrategy uniqueID. - // Used in FUSDEVStrategy.closePosition to pre-supplement the debt when yield tokens alone are - // insufficient (e.g. ~0.02% round-trip fee shortfall with no accrued yield). - - access(contract) view fun _getCollateralToDebtSwapper(_ id: UInt64): {DeFiActions.Swapper}? { - let partition = FlowYieldVaultsStrategiesV2.config["collateralToDebtSwappers"] as! {UInt64: {DeFiActions.Swapper}}? ?? {} - return partition[id] + /// Builds a collateral→MOET UniV3 swapper from CollateralConfig. + /// Contract-level equivalent of MorphoERC4626StrategyComposer._createCollateralToDebtSwapper. + access(self) fun _buildCollateralToDebtSwapper( + collateralConfig: FlowYieldVaultsStrategiesV2.CollateralConfig, + tokens: FlowYieldVaultsStrategiesV2.TokenBundle, + collateralType: Type, + uniqueID: DeFiActions.UniqueIdentifier + ): UniswapV3SwapConnectors.Swapper { + let yieldToCollPath = collateralConfig.yieldToCollateralUniV3AddressPath + let yieldToCollFees = collateralConfig.yieldToCollateralUniV3FeePath + assert(yieldToCollPath.length >= 2, message: "yieldToCollateral path must have at least 2 elements") + let collateralEVMAddress = yieldToCollPath[yieldToCollPath.length - 1] + let underlyingEVMAddress = tokens.underlying4626AssetEVMAddress + let collateralToUnderlyingFee = yieldToCollFees[yieldToCollFees.length - 1] + return FlowYieldVaultsStrategiesV2._buildUniV3Swapper( + tokenPath: [collateralEVMAddress, underlyingEVMAddress, tokens.moetTokenEVMAddress], + feePath: [collateralToUnderlyingFee, UInt32(100)], + inVault: collateralType, + outVault: tokens.moetTokenType, + uniqueID: uniqueID + ) } - access(contract) fun _setCollateralToDebtSwapper(_ id: UInt64, _ swapper: {DeFiActions.Swapper}) { - var partition = FlowYieldVaultsStrategiesV2.config["collateralToDebtSwappers"] as! {UInt64: {DeFiActions.Swapper}}? ?? {} - partition[id] = swapper - FlowYieldVaultsStrategiesV2.config["collateralToDebtSwappers"] = partition + /// Resolves the MOET→collateral swapper for closePosition dust handling. + /// Returns the swapper by value to avoid {DeFiActions.Swapper}? declaration issues at call site. + /// Standard path: MOET→YIELD→collateral (SequentialSwapper). + /// Stablecoin path: MOET→original_collateral via reversed preswap path. + access(self) fun _resolveDebtToCollateralSwapper( + uniqueID: DeFiActions.UniqueIdentifier, + collateralConfig: FlowYieldVaultsStrategiesV2.CollateralConfig, + tokens: FlowYieldVaultsStrategiesV2.TokenBundle, + collateralType: Type + ): {DeFiActions.Swapper}? { + if let origType = FlowYieldVaultsStrategiesV2._getOriginalCollateralType(uniqueID.id) { + if let preswapCfg = FlowYieldVaultsStrategiesV2._getMoetPreswapConfig( + composer: Type<@MorphoERC4626StrategyComposer>(), + collateral: origType + ) { + return FlowYieldVaultsStrategiesV2._buildUniV3Swapper( + tokenPath: preswapCfg.collateralToMoetAddressPath.reverse(), + feePath: preswapCfg.collateralToMoetFeePath.reverse(), + inVault: tokens.moetTokenType, + outVault: origType, + uniqueID: uniqueID + ) + } + return nil + } else { + return FlowYieldVaultsStrategiesV2._buildDebtToCollateralSwapper( + collateralConfig: collateralConfig, + tokens: tokens, + collateralType: collateralType, + uniqueID: uniqueID + ) + } } - access(contract) fun _removeCollateralToDebtSwapper(_ id: UInt64) { - var partition = FlowYieldVaultsStrategiesV2.config["collateralToDebtSwappers"] as! {UInt64: {DeFiActions.Swapper}}? ?? {} - partition.remove(key: id) - FlowYieldVaultsStrategiesV2.config["collateralToDebtSwappers"] = partition + /// Builds a MOET→collateral SequentialSwapper for dust handling in closePosition. + /// Chain: MOET → YIELD (AMM direct) → collateral (AMM via yieldToCollateral path). + access(self) fun _buildDebtToCollateralSwapper( + collateralConfig: FlowYieldVaultsStrategiesV2.CollateralConfig, + tokens: FlowYieldVaultsStrategiesV2.TokenBundle, + collateralType: Type, + uniqueID: DeFiActions.UniqueIdentifier + ): SwapConnectors.SequentialSwapper { + let debtToYieldAMM = FlowYieldVaultsStrategiesV2._buildUniV3Swapper( + tokenPath: [tokens.moetTokenEVMAddress, tokens.yieldTokenEVMAddress], + feePath: [100], + inVault: tokens.moetTokenType, + outVault: tokens.yieldTokenType, + uniqueID: uniqueID + ) + let yieldToCollateral = FlowYieldVaultsStrategiesV2._buildUniV3Swapper( + tokenPath: collateralConfig.yieldToCollateralUniV3AddressPath, + feePath: collateralConfig.yieldToCollateralUniV3FeePath, + inVault: tokens.yieldTokenType, + outVault: collateralType, + uniqueID: uniqueID + ) + return SwapConnectors.SequentialSwapper( + swappers: [debtToYieldAMM, yieldToCollateral], + uniqueID: uniqueID + ) } // --- "closedPositions" partition --- diff --git a/cadence/tests/transactions/provision_wbtc_from_weth.cdc b/cadence/tests/transactions/provision_wbtc_from_weth.cdc index bc7bedc3..deec64b0 100644 --- a/cadence/tests/transactions/provision_wbtc_from_weth.cdc +++ b/cadence/tests/transactions/provision_wbtc_from_weth.cdc @@ -25,7 +25,7 @@ transaction( wethAmount: UFix64 ) { prepare(signer: auth(Storage, BorrowValue, IssueStorageCapabilityController, SaveValue, PublishCapability, UnpublishCapability) &Account) { - let coaCap = signer.capabilities.storage.issue(/storage/evm) + let coaCap = signer.capabilities.storage.issue(/storage/evm) let wethEVM = EVM.addressFromString(wethEvmAddr) let wbtcEVM = EVM.addressFromString(wbtcEvmAddr) From be3dbbf01f7773ba1a90f788805594e961d3fcd8 Mon Sep 17 00:00:00 2001 From: Alex <12097569+nialexsan@users.noreply.github.com> Date: Wed, 18 Mar 2026 11:39:10 -0400 Subject: [PATCH 14/29] cleaned fusdev strategy --- .../contracts/FlowYieldVaultsStrategiesV2.cdc | 318 ++++++++---------- 1 file changed, 140 insertions(+), 178 deletions(-) diff --git a/cadence/contracts/FlowYieldVaultsStrategiesV2.cdc b/cadence/contracts/FlowYieldVaultsStrategiesV2.cdc index 0b0f3f80..197bcaee 100644 --- a/cadence/contracts/FlowYieldVaultsStrategiesV2.cdc +++ b/cadence/contracts/FlowYieldVaultsStrategiesV2.cdc @@ -179,8 +179,6 @@ access(all) contract FlowYieldVaultsStrategiesV2 { return ofToken == self.source.getSourceType() ? self.source.minimumAvailable() : 0.0 } /// Deposits up to the inner Sink's capacity from the provided authorized Vault reference. - /// Only accepts the sink's collateral type (e.g. WBTC, WETH). PYUSD0 is not accepted - /// because it is the FUSDEV vault's underlying asset, not a valid collateral token. access(all) fun deposit(from: auth(FungibleToken.Withdraw) &{FungibleToken.Vault}) { pre { from.getType() == self.sink.getSinkType(): @@ -260,22 +258,14 @@ access(all) contract FlowYieldVaultsStrategiesV2 { ?? panic("Could not create external source from AutoBalancer") // Step 5: Reconstruct yield→MOET swapper from stored CollateralConfig. - // The swapper is no longer cached in the config dict (too expensive to write as - // the debtToCollateralSwappers partition grows on mainnet). Reconstruct instead. - // - // For stablecoin path (e.g. PYUSD0 collateral → internal MOET collateral), - // _getOriginalCollateralType gives us the user-facing key for CollateralConfig lookup. - let closeCollateralKey = self.uniqueID != nil - ? FlowYieldVaultsStrategiesV2._getOriginalCollateralType(self.uniqueID!.id) ?? collateralType - : collateralType - let closeCollateralConfig = FlowYieldVaultsStrategiesV2._getStoredCollateralConfig( + let closeCollateralConfig = self._getStoredCollateralConfig( strategyType: Type<@FUSDEVStrategy>(), - collateralType: closeCollateralKey - ) ?? panic("No CollateralConfig for FUSDEVStrategy with \(closeCollateralKey.identifier)") + collateralType: collateralType + ) ?? panic("No CollateralConfig for FUSDEVStrategy with \(collateralType.identifier)") let closeTokens = FlowYieldVaultsStrategiesV2._resolveTokenBundle( yieldTokenEVMAddress: closeCollateralConfig.yieldTokenEVMAddress ) - let yieldToMoetSwapper = FlowYieldVaultsStrategiesV2._buildYieldToDebtSwapper( + let yieldToMoetSwapper = self._buildYieldToDebtSwapper( tokens: closeTokens, uniqueID: self.uniqueID! ) @@ -299,8 +289,9 @@ access(all) contract FlowYieldVaultsStrategiesV2 { ? yieldToMoetSwapper.quoteOut(forProvided: yieldAvail, reverse: false).outAmount : 0.0 if expectedMOET < totalDebtAmount { - let collateralToMoetSwapper = FlowYieldVaultsStrategiesV2._reconstructCollateralToDebtSwapper( - collateralConfig: collateralConfig, + let collateralToMoetSwapper = self._buildCollateralToDebtSwapper( + collateralConfig: closeCollateralConfig, + tokens: closeTokens, collateralType: collateralType, uniqueID: self.uniqueID! ) @@ -311,14 +302,8 @@ access(all) contract FlowYieldVaultsStrategiesV2 { if quote.inAmount > 0.0 { let extraCollateral <- self.source.withdrawAvailable(maxAmount: quote.inAmount) if extraCollateral.balance > 0.0 { - // Re-quote against the actual withdrawn amount: if the source was underfunded, - // extraCollateral.balance < quote.inAmount. Passing the stale quote to swap() - // would set amountOutMinimum = buffered (the full desired output), which the - // smaller input cannot achieve, causing the UniV3 exactInput call to revert. - let actualQuote = collateralToMoetSwapper.quoteOut(forProvided: extraCollateral.balance, reverse: false) - let extraMOET <- collateralToMoetSwapper.swap(quote: actualQuote, inVault: <-extraCollateral) + let extraMOET <- collateralToMoetSwapper.swap(quote: quote, inVault: <-extraCollateral) if extraMOET.balance > 0.0 { - // Deposit MOET to reduce position debt before close self.position.deposit(from: <-extraMOET) } else { Burner.burn(<-extraMOET) @@ -346,9 +331,8 @@ access(all) contract FlowYieldVaultsStrategiesV2 { // 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 reconstructed swapper. - // - // Standard path: reconstruct MOET→YIELD→collateral from CollateralConfig. - let debtToCollateralSwapper = FlowYieldVaultsStrategiesV2._resolveDebtToCollateralSwapper( + // Reconstruct MOET→YIELD→collateral from CollateralConfig. + let debtToCollateralSwapper = self._resolveDebtToCollateralSwapper( uniqueID: self.uniqueID!, collateralConfig: closeCollateralConfig, tokens: closeTokens, @@ -386,10 +370,6 @@ access(all) contract FlowYieldVaultsStrategiesV2 { access(contract) fun burnCallback() { FlowYieldVaultsAutoBalancers._cleanupAutoBalancer(id: self.id()!) FlowYieldVaultsStrategiesV2._cleanupPositionClosed(self.uniqueID) - // Clean up stablecoin pre-swap config entries (no-op if not set) - if let id = self.uniqueID { - FlowYieldVaultsStrategiesV2._removeDebtToCollateralSwapper(id.id) - } } access(all) fun getComponentInfo(): DeFiActions.ComponentInfo { return DeFiActions.ComponentInfo( @@ -407,6 +387,123 @@ access(all) contract FlowYieldVaultsStrategiesV2 { access(contract) fun setID(_ id: DeFiActions.UniqueIdentifier?) { self.uniqueID = id } + + /* =========================== + closePosition helpers + =========================== */ + + access(self) fun _getStoredCollateralConfig( + strategyType: Type, + collateralType: Type + ): CollateralConfig? { + let issuer = FlowYieldVaultsStrategiesV2.account.storage.borrow< + &FlowYieldVaultsStrategiesV2.StrategyComposerIssuer + >(from: FlowYieldVaultsStrategiesV2.IssuerStoragePath) + if issuer == nil { return nil } + return issuer!.getCollateralConfig(strategyType: strategyType, collateralType: collateralType) + } + + /// Builds a YIELD→MOET MultiSwapper (AMM direct + ERC4626 redeem path). + access(self) fun _buildYieldToDebtSwapper( + tokens: FlowYieldVaultsStrategiesV2.TokenBundle, + uniqueID: DeFiActions.UniqueIdentifier + ): SwapConnectors.MultiSwapper { + let yieldToDebtAMM = FlowYieldVaultsStrategiesV2._buildUniV3Swapper( + tokenPath: [tokens.yieldTokenEVMAddress, tokens.moetTokenEVMAddress], + feePath: [100], + inVault: tokens.yieldTokenType, + outVault: tokens.moetTokenType, + uniqueID: uniqueID + ) + let yieldToUnderlying = MorphoERC4626SwapConnectors.Swapper( + vaultEVMAddress: tokens.yieldTokenEVMAddress, + coa: FlowYieldVaultsStrategiesV2._getCOACapability(), + feeSource: FlowYieldVaultsStrategiesV2._createFeeSource(withID: uniqueID), + uniqueID: uniqueID, + isReversed: true + ) + let underlyingToDebt = FlowYieldVaultsStrategiesV2._buildUniV3Swapper( + tokenPath: [tokens.underlying4626AssetEVMAddress, tokens.moetTokenEVMAddress], + feePath: [100], + inVault: tokens.underlying4626AssetType, + outVault: tokens.moetTokenType, + uniqueID: uniqueID + ) + let seq = SwapConnectors.SequentialSwapper( + swappers: [yieldToUnderlying, underlyingToDebt], + uniqueID: uniqueID + ) + return SwapConnectors.MultiSwapper( + inVault: tokens.yieldTokenType, + outVault: tokens.moetTokenType, + swappers: [yieldToDebtAMM, seq], + uniqueID: uniqueID + ) + } + + /// Builds a collateral→MOET UniV3 swapper from CollateralConfig. + access(self) fun _buildCollateralToDebtSwapper( + collateralConfig: FlowYieldVaultsStrategiesV2.CollateralConfig, + tokens: FlowYieldVaultsStrategiesV2.TokenBundle, + collateralType: Type, + uniqueID: DeFiActions.UniqueIdentifier + ): UniswapV3SwapConnectors.Swapper { + let yieldToCollPath = collateralConfig.yieldToCollateralUniV3AddressPath + let yieldToCollFees = collateralConfig.yieldToCollateralUniV3FeePath + assert(yieldToCollPath.length >= 2, message: "yieldToCollateral path must have at least 2 elements") + let collateralEVMAddress = yieldToCollPath[yieldToCollPath.length - 1] + let underlyingEVMAddress = tokens.underlying4626AssetEVMAddress + let collateralToUnderlyingFee = yieldToCollFees[yieldToCollFees.length - 1] + return FlowYieldVaultsStrategiesV2._buildUniV3Swapper( + tokenPath: [collateralEVMAddress, underlyingEVMAddress, tokens.moetTokenEVMAddress], + feePath: [collateralToUnderlyingFee, UInt32(100)], + inVault: collateralType, + outVault: tokens.moetTokenType, + uniqueID: uniqueID + ) + } + + /// Builds a MOET→collateral SequentialSwapper for dust handling: MOET→YIELD→collateral. + access(self) fun _buildDebtToCollateralSwapper( + collateralConfig: FlowYieldVaultsStrategiesV2.CollateralConfig, + tokens: FlowYieldVaultsStrategiesV2.TokenBundle, + collateralType: Type, + uniqueID: DeFiActions.UniqueIdentifier + ): SwapConnectors.SequentialSwapper { + let debtToYieldAMM = FlowYieldVaultsStrategiesV2._buildUniV3Swapper( + tokenPath: [tokens.moetTokenEVMAddress, tokens.yieldTokenEVMAddress], + feePath: [100], + inVault: tokens.moetTokenType, + outVault: tokens.yieldTokenType, + uniqueID: uniqueID + ) + let yieldToCollateral = FlowYieldVaultsStrategiesV2._buildUniV3Swapper( + tokenPath: collateralConfig.yieldToCollateralUniV3AddressPath, + feePath: collateralConfig.yieldToCollateralUniV3FeePath, + inVault: tokens.yieldTokenType, + outVault: collateralType, + uniqueID: uniqueID + ) + return SwapConnectors.SequentialSwapper( + swappers: [debtToYieldAMM, yieldToCollateral], + uniqueID: uniqueID + ) + } + + /// Resolves the MOET→collateral swapper for closePosition dust handling. + access(self) fun _resolveDebtToCollateralSwapper( + uniqueID: DeFiActions.UniqueIdentifier, + collateralConfig: FlowYieldVaultsStrategiesV2.CollateralConfig, + tokens: FlowYieldVaultsStrategiesV2.TokenBundle, + collateralType: Type + ): {DeFiActions.Swapper}? { + return self._buildDebtToCollateralSwapper( + collateralConfig: collateralConfig, + tokens: tokens, + collateralType: collateralType, + uniqueID: uniqueID + ) + } } access(all) struct TokenBundle { @@ -664,6 +761,9 @@ access(all) contract FlowYieldVaultsStrategiesV2 { source: balancerIO.source, uniqueID: uniqueID ) + + // --- Standard path (WBTC, WETH, WFLOW — directly supported by FlowALP) --- + // Open FlowALPv0 position let position <- FlowYieldVaultsStrategiesV2._openCreditPosition( funds: <-withFunds, @@ -998,11 +1098,19 @@ access(all) contract FlowYieldVaultsStrategiesV2 { return false } + access(all) view fun getSupportedComposers(): {Type: Bool} { + return { + Type<@MorphoERC4626StrategyComposer>(): true + } + } + + /// Returns CollateralConfig for the given strategy+collateral, by value (not reference). + /// Called from contract-level _getStoredCollateralConfig to avoid reference-chain issues. access(all) fun getCollateralConfig( - composerType: Type, strategyType: Type, collateralType: Type ): CollateralConfig? { + let composerType = Type<@MorphoERC4626StrategyComposer>() if let p0 = self.configs[composerType] { if let p1 = p0[strategyType] { return p1[collateralType] @@ -1011,12 +1119,6 @@ access(all) contract FlowYieldVaultsStrategiesV2 { return nil } - access(all) view fun getSupportedComposers(): {Type: Bool} { - return { - Type<@MorphoERC4626StrategyComposer>(): true - } - } - access(self) view fun isSupportedComposer(_ type: Type): Bool { return type == Type<@MorphoERC4626StrategyComposer>() } @@ -1158,23 +1260,7 @@ access(all) contract FlowYieldVaultsStrategiesV2 { ) } - // --- Reconstruction helpers --- - // These allow closePosition to rebuild swappers from stored CollateralConfig at close time, - // avoiding the expensive per-position config dict writes during createStrategy. - - /// Reads CollateralConfig from StrategyComposerIssuer, returning a value copy. - access(self) fun _getStoredCollateralConfig( - strategyType: Type, - collateralType: Type - ): CollateralConfig? { - let issuer = FlowYieldVaultsStrategiesV2.account.storage.borrow< - &FlowYieldVaultsStrategiesV2.StrategyComposerIssuer - >(from: FlowYieldVaultsStrategiesV2.IssuerStoragePath) - if issuer == nil { return nil } - return issuer!.getCollateralConfig(strategyType: strategyType, collateralType: collateralType) - } - - /// Builds a UniswapV3 swapper. Contract-level equivalent of MorphoERC4626StrategyComposer._createUniV3Swapper. + /// Builds a UniswapV3 swapper. Shared by FUSDEVStrategy and syWFLOWvStrategy closePosition helpers. access(self) fun _buildUniV3Swapper( tokenPath: [EVM.EVMAddress], feePath: [UInt32], @@ -1195,130 +1281,6 @@ access(all) contract FlowYieldVaultsStrategiesV2 { ) } - /// Builds a YIELD→MOET MultiSwapper from a TokenBundle. - /// Contract-level equivalent of MorphoERC4626StrategyComposer._createYieldToDebtSwapper. - access(self) fun _buildYieldToDebtSwapper( - tokens: FlowYieldVaultsStrategiesV2.TokenBundle, - uniqueID: DeFiActions.UniqueIdentifier - ): SwapConnectors.MultiSwapper { - let yieldToDebtAMM = FlowYieldVaultsStrategiesV2._buildUniV3Swapper( - tokenPath: [tokens.yieldTokenEVMAddress, tokens.moetTokenEVMAddress], - feePath: [100], - inVault: tokens.yieldTokenType, - outVault: tokens.moetTokenType, - uniqueID: uniqueID - ) - let yieldToUnderlying = MorphoERC4626SwapConnectors.Swapper( - vaultEVMAddress: tokens.yieldTokenEVMAddress, - coa: FlowYieldVaultsStrategiesV2._getCOACapability(), - feeSource: FlowYieldVaultsStrategiesV2._createFeeSource(withID: uniqueID), - uniqueID: uniqueID, - isReversed: true - ) - let underlyingToDebt = FlowYieldVaultsStrategiesV2._buildUniV3Swapper( - tokenPath: [tokens.underlying4626AssetEVMAddress, tokens.moetTokenEVMAddress], - feePath: [100], - inVault: tokens.underlying4626AssetType, - outVault: tokens.moetTokenType, - uniqueID: uniqueID - ) - let seq = SwapConnectors.SequentialSwapper( - swappers: [yieldToUnderlying, underlyingToDebt], - uniqueID: uniqueID - ) - return SwapConnectors.MultiSwapper( - inVault: tokens.yieldTokenType, - outVault: tokens.moetTokenType, - swappers: [yieldToDebtAMM, seq], - uniqueID: uniqueID - ) - } - - /// Builds a collateral→MOET UniV3 swapper from CollateralConfig. - /// Contract-level equivalent of MorphoERC4626StrategyComposer._createCollateralToDebtSwapper. - access(self) fun _buildCollateralToDebtSwapper( - collateralConfig: FlowYieldVaultsStrategiesV2.CollateralConfig, - tokens: FlowYieldVaultsStrategiesV2.TokenBundle, - collateralType: Type, - uniqueID: DeFiActions.UniqueIdentifier - ): UniswapV3SwapConnectors.Swapper { - let yieldToCollPath = collateralConfig.yieldToCollateralUniV3AddressPath - let yieldToCollFees = collateralConfig.yieldToCollateralUniV3FeePath - assert(yieldToCollPath.length >= 2, message: "yieldToCollateral path must have at least 2 elements") - let collateralEVMAddress = yieldToCollPath[yieldToCollPath.length - 1] - let underlyingEVMAddress = tokens.underlying4626AssetEVMAddress - let collateralToUnderlyingFee = yieldToCollFees[yieldToCollFees.length - 1] - return FlowYieldVaultsStrategiesV2._buildUniV3Swapper( - tokenPath: [collateralEVMAddress, underlyingEVMAddress, tokens.moetTokenEVMAddress], - feePath: [collateralToUnderlyingFee, UInt32(100)], - inVault: collateralType, - outVault: tokens.moetTokenType, - uniqueID: uniqueID - ) - } - - /// Resolves the MOET→collateral swapper for closePosition dust handling. - /// Returns the swapper by value to avoid {DeFiActions.Swapper}? declaration issues at call site. - /// Standard path: MOET→YIELD→collateral (SequentialSwapper). - /// Stablecoin path: MOET→original_collateral via reversed preswap path. - access(self) fun _resolveDebtToCollateralSwapper( - uniqueID: DeFiActions.UniqueIdentifier, - collateralConfig: FlowYieldVaultsStrategiesV2.CollateralConfig, - tokens: FlowYieldVaultsStrategiesV2.TokenBundle, - collateralType: Type - ): {DeFiActions.Swapper}? { - if let origType = FlowYieldVaultsStrategiesV2._getOriginalCollateralType(uniqueID.id) { - if let preswapCfg = FlowYieldVaultsStrategiesV2._getMoetPreswapConfig( - composer: Type<@MorphoERC4626StrategyComposer>(), - collateral: origType - ) { - return FlowYieldVaultsStrategiesV2._buildUniV3Swapper( - tokenPath: preswapCfg.collateralToMoetAddressPath.reverse(), - feePath: preswapCfg.collateralToMoetFeePath.reverse(), - inVault: tokens.moetTokenType, - outVault: origType, - uniqueID: uniqueID - ) - } - return nil - } else { - return FlowYieldVaultsStrategiesV2._buildDebtToCollateralSwapper( - collateralConfig: collateralConfig, - tokens: tokens, - collateralType: collateralType, - uniqueID: uniqueID - ) - } - } - - /// Builds a MOET→collateral SequentialSwapper for dust handling in closePosition. - /// Chain: MOET → YIELD (AMM direct) → collateral (AMM via yieldToCollateral path). - access(self) fun _buildDebtToCollateralSwapper( - collateralConfig: FlowYieldVaultsStrategiesV2.CollateralConfig, - tokens: FlowYieldVaultsStrategiesV2.TokenBundle, - collateralType: Type, - uniqueID: DeFiActions.UniqueIdentifier - ): SwapConnectors.SequentialSwapper { - let debtToYieldAMM = FlowYieldVaultsStrategiesV2._buildUniV3Swapper( - tokenPath: [tokens.moetTokenEVMAddress, tokens.yieldTokenEVMAddress], - feePath: [100], - inVault: tokens.moetTokenType, - outVault: tokens.yieldTokenType, - uniqueID: uniqueID - ) - let yieldToCollateral = FlowYieldVaultsStrategiesV2._buildUniV3Swapper( - tokenPath: collateralConfig.yieldToCollateralUniV3AddressPath, - feePath: collateralConfig.yieldToCollateralUniV3FeePath, - inVault: tokens.yieldTokenType, - outVault: collateralType, - uniqueID: uniqueID - ) - return SwapConnectors.SequentialSwapper( - swappers: [debtToYieldAMM, yieldToCollateral], - uniqueID: uniqueID - ) - } - // --- "closedPositions" partition --- access(contract) view fun _isPositionClosed(_ uniqueID: DeFiActions.UniqueIdentifier?): Bool { From fc38a293633eccf1ce7d73f27772ab85fa41d2b6 Mon Sep 17 00:00:00 2001 From: Alex <12097569+nialexsan@users.noreply.github.com> Date: Wed, 18 Mar 2026 12:40:02 -0400 Subject: [PATCH 15/29] tweak coa cap --- cadence/tests/transactions/provision_wbtc_from_weth.cdc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cadence/tests/transactions/provision_wbtc_from_weth.cdc b/cadence/tests/transactions/provision_wbtc_from_weth.cdc index deec64b0..0f9289d9 100644 --- a/cadence/tests/transactions/provision_wbtc_from_weth.cdc +++ b/cadence/tests/transactions/provision_wbtc_from_weth.cdc @@ -25,7 +25,7 @@ transaction( wethAmount: UFix64 ) { prepare(signer: auth(Storage, BorrowValue, IssueStorageCapabilityController, SaveValue, PublishCapability, UnpublishCapability) &Account) { - let coaCap = signer.capabilities.storage.issue(/storage/evm) + let coaCap = signer.capabilities.storage.issue(/storage/evm) let wethEVM = EVM.addressFromString(wethEvmAddr) let wbtcEVM = EVM.addressFromString(wbtcEvmAddr) From 55adc6e61af42f5ffc1f791d56dbbf61da2a1bf4 Mon Sep 17 00:00:00 2001 From: Alex <12097569+nialexsan@users.noreply.github.com> Date: Wed, 18 Mar 2026 12:54:56 -0400 Subject: [PATCH 16/29] apply suggestions --- .../contracts/FlowYieldVaultsStrategiesV2.cdc | 23 ++++--------------- 1 file changed, 4 insertions(+), 19 deletions(-) diff --git a/cadence/contracts/FlowYieldVaultsStrategiesV2.cdc b/cadence/contracts/FlowYieldVaultsStrategiesV2.cdc index 197bcaee..c3f0d8d4 100644 --- a/cadence/contracts/FlowYieldVaultsStrategiesV2.cdc +++ b/cadence/contracts/FlowYieldVaultsStrategiesV2.cdc @@ -332,11 +332,11 @@ access(all) contract FlowYieldVaultsStrategiesV2 { // assume the collateral vault is first. Find it by type and convert any non-collateral // vaults (MOET overpayment dust) back to collateral via reconstructed swapper. // Reconstruct MOET→YIELD→collateral from CollateralConfig. - let debtToCollateralSwapper = self._resolveDebtToCollateralSwapper( - uniqueID: self.uniqueID!, + let debtToCollateralSwapper = self._buildDebtToCollateralSwapper( collateralConfig: closeCollateralConfig, tokens: closeTokens, - collateralType: collateralType + collateralType: collateralType, + uniqueID: self.uniqueID! ) var collateralVault <- DeFiActionsUtils.getEmptyVault(collateralType) @@ -489,21 +489,6 @@ access(all) contract FlowYieldVaultsStrategiesV2 { uniqueID: uniqueID ) } - - /// Resolves the MOET→collateral swapper for closePosition dust handling. - access(self) fun _resolveDebtToCollateralSwapper( - uniqueID: DeFiActions.UniqueIdentifier, - collateralConfig: FlowYieldVaultsStrategiesV2.CollateralConfig, - tokens: FlowYieldVaultsStrategiesV2.TokenBundle, - collateralType: Type - ): {DeFiActions.Swapper}? { - return self._buildDebtToCollateralSwapper( - collateralConfig: collateralConfig, - tokens: tokens, - collateralType: collateralType, - uniqueID: uniqueID - ) - } } access(all) struct TokenBundle { @@ -838,7 +823,7 @@ access(all) contract FlowYieldVaultsStrategiesV2 { ) return strategyConfig[collateralType] - ?? panic("Could not find config for collateral \(collateralType.identifier)") + ?? panic("Could not find config for collateral \(collateralType.identifier) when creating Strategy \(strategyType.identifier)") } access(self) fun _createUniV3Swapper( From 2ca5bd15032c41a53dc66682c3136eff20ba1b21 Mon Sep 17 00:00:00 2001 From: Alex <12097569+nialexsan@users.noreply.github.com> Date: Wed, 18 Mar 2026 12:57:07 -0400 Subject: [PATCH 17/29] add panic to empty config --- cadence/contracts/FlowYieldVaultsStrategiesV2.cdc | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/cadence/contracts/FlowYieldVaultsStrategiesV2.cdc b/cadence/contracts/FlowYieldVaultsStrategiesV2.cdc index c3f0d8d4..dc815cb9 100644 --- a/cadence/contracts/FlowYieldVaultsStrategiesV2.cdc +++ b/cadence/contracts/FlowYieldVaultsStrategiesV2.cdc @@ -1114,7 +1114,9 @@ access(all) contract FlowYieldVaultsStrategiesV2 { } switch type { case Type<@MorphoERC4626StrategyComposer>(): - return <- create MorphoERC4626StrategyComposer(self.configs[type] ?? {}) + return <- create MorphoERC4626StrategyComposer( + self.configs[type] ?? panic("No config registered for \(type.identifier)") + ) default: panic("Unsupported StrategyComposer \(type.identifier) requested") } From 417ded3d7bba6fb53ba0f24f8ab77f39cc78e615 Mon Sep 17 00:00:00 2001 From: Alex <12097569+nialexsan@users.noreply.github.com> Date: Wed, 18 Mar 2026 12:59:47 -0400 Subject: [PATCH 18/29] fix optional --- cadence/contracts/FlowYieldVaultsStrategiesV2.cdc | 14 +++++--------- 1 file changed, 5 insertions(+), 9 deletions(-) diff --git a/cadence/contracts/FlowYieldVaultsStrategiesV2.cdc b/cadence/contracts/FlowYieldVaultsStrategiesV2.cdc index dc815cb9..2d69ad88 100644 --- a/cadence/contracts/FlowYieldVaultsStrategiesV2.cdc +++ b/cadence/contracts/FlowYieldVaultsStrategiesV2.cdc @@ -345,15 +345,11 @@ access(all) contract FlowYieldVaultsStrategiesV2 { 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) - if quote.outAmount > 0.0 { - let swapped <- swapper.swap(quote: quote, inVault: <-v) - collateralVault.deposit(from: <-swapped) - } else { - Burner.burn(<-v) - } + // Quote first — if dust is too small to route, destroy it + let quote = debtToCollateralSwapper.quoteOut(forProvided: v.balance, reverse: false) + if quote.outAmount > 0.0 { + let swapped <- debtToCollateralSwapper.swap(quote: quote, inVault: <-v) + collateralVault.deposit(from: <-swapped) } else { Burner.burn(<-v) } From a6ee1092e70f693c7deed4e28488c1a0972e1568 Mon Sep 17 00:00:00 2001 From: Alex <12097569+nialexsan@users.noreply.github.com> Date: Wed, 18 Mar 2026 15:57:02 -0400 Subject: [PATCH 19/29] safeguards --- .../contracts/FlowYieldVaultsStrategiesV2.cdc | 105 ++++++++++-------- 1 file changed, 59 insertions(+), 46 deletions(-) diff --git a/cadence/contracts/FlowYieldVaultsStrategiesV2.cdc b/cadence/contracts/FlowYieldVaultsStrategiesV2.cdc index 2d69ad88..d37d450d 100644 --- a/cadence/contracts/FlowYieldVaultsStrategiesV2.cdc +++ b/cadence/contracts/FlowYieldVaultsStrategiesV2.cdc @@ -114,7 +114,9 @@ access(all) contract FlowYieldVaultsStrategiesV2 { Burner.burn(<-sourceLiquidity) return <- DeFiActionsUtils.getEmptyVault(self.getSourceType()) } - return <- self.swapper.swap(quote: nil, inVault: <-sourceLiquidity) + let swapped <- self.swapper.swap(quote: nil, inVault: <-sourceLiquidity) + assert(swapped.balance > 0.0, message: "BufferedSwapSource: swap returned zero despite available input") + return <- swapped } } @@ -175,7 +177,7 @@ access(all) contract FlowYieldVaultsStrategiesV2 { } /// Returns the amount available for withdrawal via the inner Source access(all) fun availableBalance(ofToken: Type): UFix64 { - if FlowYieldVaultsStrategiesV2._isPositionClosed(self.uniqueID) { return 0.0 } + if self._isPositionClosed() { return 0.0 } return ofToken == self.source.getSourceType() ? self.source.minimumAvailable() : 0.0 } /// Deposits up to the inner Sink's capacity from the provided authorized Vault reference. @@ -244,12 +246,12 @@ access(all) contract FlowYieldVaultsStrategiesV2 { // Zero vaults: dust collateral rounded down to zero — return an empty vault if resultVaults.length == 0 { destroy resultVaults - FlowYieldVaultsStrategiesV2._markPositionClosed(self.uniqueID) + self._markPositionClosed() return <- DeFiActionsUtils.getEmptyVault(collateralType) } var collateralVault <- resultVaults.removeFirst() destroy resultVaults - FlowYieldVaultsStrategiesV2._markPositionClosed(self.uniqueID) + self._markPositionClosed() return <- collateralVault } @@ -301,16 +303,12 @@ access(all) contract FlowYieldVaultsStrategiesV2 { let quote = collateralToMoetSwapper.quoteIn(forDesired: buffered, reverse: false) if quote.inAmount > 0.0 { let extraCollateral <- self.source.withdrawAvailable(maxAmount: quote.inAmount) - if extraCollateral.balance > 0.0 { - let extraMOET <- collateralToMoetSwapper.swap(quote: quote, inVault: <-extraCollateral) - if extraMOET.balance > 0.0 { - self.position.deposit(from: <-extraMOET) - } else { - Burner.burn(<-extraMOET) - } - } else { - Burner.burn(<-extraCollateral) - } + assert(extraCollateral.balance > 0.0, + message: "Pre-supplement: no collateral available to cover shortfall of \(shortfall) MOET") + let extraMOET <- collateralToMoetSwapper.swap(quote: quote, inVault: <-extraCollateral) + assert(extraMOET.balance > 0.0, + message: "Pre-supplement: collateral→MOET swap produced zero output") + self.position.deposit(from: <-extraMOET) } } @@ -359,13 +357,13 @@ access(all) contract FlowYieldVaultsStrategiesV2 { } destroy resultVaults - FlowYieldVaultsStrategiesV2._markPositionClosed(self.uniqueID) + self._markPositionClosed() return <- collateralVault } /// Executed when a Strategy is burned, cleaning up the Strategy's stored AutoBalancer access(contract) fun burnCallback() { FlowYieldVaultsAutoBalancers._cleanupAutoBalancer(id: self.id()!) - FlowYieldVaultsStrategiesV2._cleanupPositionClosed(self.uniqueID) + self._cleanupPositionClosed() } access(all) fun getComponentInfo(): DeFiActions.ComponentInfo { return DeFiActions.ComponentInfo( @@ -438,6 +436,9 @@ access(all) contract FlowYieldVaultsStrategiesV2 { } /// Builds a collateral→MOET UniV3 swapper from CollateralConfig. + /// Derives the path by reversing yieldToCollateralUniV3AddressPath[1..] (skipping the + /// yield token) and appending MOET, preserving all intermediate hops. + /// e.g. [FUSDEV, PYUSD0, WETH, WBTC] → [WBTC, WETH, PYUSD0, MOET] access(self) fun _buildCollateralToDebtSwapper( collateralConfig: FlowYieldVaultsStrategiesV2.CollateralConfig, tokens: FlowYieldVaultsStrategiesV2.TokenBundle, @@ -447,12 +448,23 @@ access(all) contract FlowYieldVaultsStrategiesV2 { let yieldToCollPath = collateralConfig.yieldToCollateralUniV3AddressPath let yieldToCollFees = collateralConfig.yieldToCollateralUniV3FeePath assert(yieldToCollPath.length >= 2, message: "yieldToCollateral path must have at least 2 elements") - let collateralEVMAddress = yieldToCollPath[yieldToCollPath.length - 1] - let underlyingEVMAddress = tokens.underlying4626AssetEVMAddress - let collateralToUnderlyingFee = yieldToCollFees[yieldToCollFees.length - 1] + // Build reversed path: iterate yieldToCollPath from last down to index 1 (skip yield token at 0), + // then append MOET. e.g. [FUSDEV, PYUSD0, WETH, WBTC] → [WBTC, WETH, PYUSD0] + MOET + var collToDebtPath: [EVM.EVMAddress] = [] + var collToDebtFees: [UInt32] = [] + for i in InclusiveRange(yieldToCollPath.length - 1, 1, step: -1) { + collToDebtPath.append(yieldToCollPath[i]) + } + collToDebtPath.append(tokens.moetTokenEVMAddress) + // Build reversed fees: iterate from last down to index 1 (skip yield→underlying fee at 0), + // then append PYUSD0→MOET fee (100). e.g. [100, 3000, 3000] → [3000, 3000] + 100 + for i in InclusiveRange(yieldToCollFees.length - 1, 1, step: -1) { + collToDebtFees.append(yieldToCollFees[i]) + } + collToDebtFees.append(UInt32(100)) return FlowYieldVaultsStrategiesV2._buildUniV3Swapper( - tokenPath: [collateralEVMAddress, underlyingEVMAddress, tokens.moetTokenEVMAddress], - feePath: [collateralToUnderlyingFee, UInt32(100)], + tokenPath: collToDebtPath, + feePath: collToDebtFees, inVault: collateralType, outVault: tokens.moetTokenType, uniqueID: uniqueID @@ -485,6 +497,30 @@ access(all) contract FlowYieldVaultsStrategiesV2 { uniqueID: uniqueID ) } + + access(self) view fun _isPositionClosed(): Bool { + if let id = self.uniqueID { + let partition = FlowYieldVaultsStrategiesV2.config["closedPositions"] as! {UInt64: Bool}? ?? {} + return partition[id.id] ?? false + } + return false + } + + access(self) fun _markPositionClosed() { + if let id = self.uniqueID { + var partition = FlowYieldVaultsStrategiesV2.config["closedPositions"] as! {UInt64: Bool}? ?? {} + partition[id.id] = true + FlowYieldVaultsStrategiesV2.config["closedPositions"] = partition + } + } + + access(self) fun _cleanupPositionClosed() { + if let id = self.uniqueID { + var partition = FlowYieldVaultsStrategiesV2.config["closedPositions"] as! {UInt64: Bool}? ?? {} + partition.remove(key: id.id) + FlowYieldVaultsStrategiesV2.config["closedPositions"] = partition + } + } } access(all) struct TokenBundle { @@ -691,6 +727,7 @@ access(all) contract FlowYieldVaultsStrategiesV2 { ): @{FlowYieldVaults.Strategy} { pre { self.config[type] != nil: "Unsupported strategy type \(type.identifier)" + self.config[type]!.length > 0: "No collateral configured for strategy type \(type.identifier)" } let collateralType = withFunds.getType() @@ -819,7 +856,7 @@ access(all) contract FlowYieldVaultsStrategiesV2 { ) return strategyConfig[collateralType] - ?? panic("Could not find config for collateral \(collateralType.identifier) when creating Strategy \(strategyType.identifier)") + ?? panic("Could not find config for collateral \(collateralType.identifier)") } access(self) fun _createUniV3Swapper( @@ -1264,30 +1301,6 @@ access(all) contract FlowYieldVaultsStrategiesV2 { ) } - // --- "closedPositions" partition --- - - access(contract) view fun _isPositionClosed(_ uniqueID: DeFiActions.UniqueIdentifier?): Bool { - if uniqueID == nil { return false } - let partition = FlowYieldVaultsStrategiesV2.config["closedPositions"] as! {UInt64: Bool}? ?? {} - return partition[uniqueID!.id] ?? false - } - - access(contract) fun _markPositionClosed(_ uniqueID: DeFiActions.UniqueIdentifier?) { - if let id = uniqueID { - var partition = FlowYieldVaultsStrategiesV2.config["closedPositions"] as! {UInt64: Bool}? ?? {} - partition[id.id] = true - FlowYieldVaultsStrategiesV2.config["closedPositions"] = partition - } - } - - access(contract) fun _cleanupPositionClosed(_ uniqueID: DeFiActions.UniqueIdentifier?) { - if let id = uniqueID { - var partition = FlowYieldVaultsStrategiesV2.config["closedPositions"] as! {UInt64: Bool}? ?? {} - partition.remove(key: id.id) - FlowYieldVaultsStrategiesV2.config["closedPositions"] = partition - } - } - init( univ3FactoryEVMAddress: String, univ3RouterEVMAddress: String, From 5268aeb340be7500eb345441a372b4cebd482ee2 Mon Sep 17 00:00:00 2001 From: Alex <12097569+nialexsan@users.noreply.github.com> Date: Mon, 23 Mar 2026 12:18:42 -0400 Subject: [PATCH 20/29] address PR comments --- .../contracts/FlowYieldVaultsStrategiesV2.cdc | 50 ++++++++++++++----- 1 file changed, 37 insertions(+), 13 deletions(-) diff --git a/cadence/contracts/FlowYieldVaultsStrategiesV2.cdc b/cadence/contracts/FlowYieldVaultsStrategiesV2.cdc index d37d450d..ce229933 100644 --- a/cadence/contracts/FlowYieldVaultsStrategiesV2.cdc +++ b/cadence/contracts/FlowYieldVaultsStrategiesV2.cdc @@ -51,6 +51,19 @@ access(all) contract FlowYieldVaultsStrategiesV2 { /// Canonical StoragePath where the StrategyComposerIssuer should be stored access(all) let IssuerStoragePath: StoragePath + /// Emitted when a non-empty vault is destroyed because the swapper quote returned zero output, + /// indicating the balance is too small to route (dust). Includes the quote as evidence of why + /// the burn decision was made, to aid debugging of stale or misconfigured swapper paths. + access(all) event DustBurned( + tokenType: String, + balance: UFix64, + quoteInType: String, + quoteOutType: String, + quoteInAmount: UFix64, + quoteOutAmount: UFix64, + swapperType: String + ) + /// A Source that converts yield tokens to debt tokens by pulling ALL available yield /// tokens from the wrapped source, rather than using quoteIn to limit the pull amount. /// @@ -298,18 +311,19 @@ access(all) contract FlowYieldVaultsStrategiesV2 { uniqueID: self.uniqueID! ) let shortfall = totalDebtAmount - expectedMOET - // Add 1% buffer to account for swap slippage/rounding in the collateral→MOET leg + // Over-deposit by 1% so the remaining debt lands below expectedMOET, giving + // BufferedSwapSource enough margin to cover ERC4626 floor-rounding at redemption let buffered = shortfall + shortfall / 100.0 let quote = collateralToMoetSwapper.quoteIn(forDesired: buffered, reverse: false) - if quote.inAmount > 0.0 { - let extraCollateral <- self.source.withdrawAvailable(maxAmount: quote.inAmount) - assert(extraCollateral.balance > 0.0, - message: "Pre-supplement: no collateral available to cover shortfall of \(shortfall) MOET") - let extraMOET <- collateralToMoetSwapper.swap(quote: quote, inVault: <-extraCollateral) - assert(extraMOET.balance > 0.0, - message: "Pre-supplement: collateral→MOET swap produced zero output") - self.position.deposit(from: <-extraMOET) - } + assert(quote.inAmount > 0.0, + message: "Pre-supplement: collateral→MOET quote returned zero input for non-zero shortfall — swapper misconfigured") + let extraCollateral <- self.source.withdrawAvailable(maxAmount: quote.inAmount) + assert(extraCollateral.balance > 0.0, + message: "Pre-supplement: no collateral available to cover shortfall of \(shortfall) MOET") + let extraMOET <- collateralToMoetSwapper.swap(quote: quote, inVault: <-extraCollateral) + assert(extraMOET.balance > 0.0, + message: "Pre-supplement: collateral→MOET swap produced zero output") + self.position.deposit(from: <-extraMOET) } // Step 7: Create a BufferedSwapSource that converts ALL yield tokens → MOET. @@ -342,17 +356,27 @@ access(all) contract FlowYieldVaultsStrategiesV2 { let v <- resultVaults.removeFirst() if v.getType() == collateralType { collateralVault.deposit(from: <-v) - } else if v.balance > 0.0 { + } else if v.balance == 0 { + // destroy empty vault + Burner.burn(<-v) + } else { // Quote first — if dust is too small to route, destroy it let quote = debtToCollateralSwapper.quoteOut(forProvided: v.balance, reverse: false) if quote.outAmount > 0.0 { let swapped <- debtToCollateralSwapper.swap(quote: quote, inVault: <-v) collateralVault.deposit(from: <-swapped) } else { + emit DustBurned( + tokenType: v.getType().identifier, + balance: v.balance, + quoteInType: quote.inType.identifier, + quoteOutType: quote.outType.identifier, + quoteInAmount: quote.inAmount, + quoteOutAmount: quote.outAmount, + swapperType: debtToCollateralSwapper.getType().identifier + ) Burner.burn(<-v) } - } else { - Burner.burn(<-v) } } From 0befc0776f4555f01fd903c52fa14835ea8aac46 Mon Sep 17 00:00:00 2001 From: Alex <12097569+nialexsan@users.noreply.github.com> Date: Mon, 23 Mar 2026 12:24:03 -0400 Subject: [PATCH 21/29] fix typo --- cadence/contracts/FlowYieldVaultsStrategiesV2.cdc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cadence/contracts/FlowYieldVaultsStrategiesV2.cdc b/cadence/contracts/FlowYieldVaultsStrategiesV2.cdc index ce229933..25c3266e 100644 --- a/cadence/contracts/FlowYieldVaultsStrategiesV2.cdc +++ b/cadence/contracts/FlowYieldVaultsStrategiesV2.cdc @@ -356,7 +356,7 @@ access(all) contract FlowYieldVaultsStrategiesV2 { let v <- resultVaults.removeFirst() if v.getType() == collateralType { collateralVault.deposit(from: <-v) - } else if v.balance == 0 { + } else if v.balance == 0.0 { // destroy empty vault Burner.burn(<-v) } else { From 5f05e4a2ef11fb68086cfc72862d3d15079990ff Mon Sep 17 00:00:00 2001 From: Alex <12097569+nialexsan@users.noreply.github.com> Date: Mon, 23 Mar 2026 12:38:46 -0400 Subject: [PATCH 22/29] fix assertion message --- cadence/contracts/FlowYieldVaultsStrategiesV2.cdc | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cadence/contracts/FlowYieldVaultsStrategiesV2.cdc b/cadence/contracts/FlowYieldVaultsStrategiesV2.cdc index 25c3266e..0928b99f 100644 --- a/cadence/contracts/FlowYieldVaultsStrategiesV2.cdc +++ b/cadence/contracts/FlowYieldVaultsStrategiesV2.cdc @@ -471,7 +471,7 @@ access(all) contract FlowYieldVaultsStrategiesV2 { ): UniswapV3SwapConnectors.Swapper { let yieldToCollPath = collateralConfig.yieldToCollateralUniV3AddressPath let yieldToCollFees = collateralConfig.yieldToCollateralUniV3FeePath - assert(yieldToCollPath.length >= 2, message: "yieldToCollateral path must have at least 2 elements") + assert(yieldToCollPath.length >= 2, message: "yieldToCollateral path requires at least yield and collateral tokens, got \(yieldToCollPath.length)") // Build reversed path: iterate yieldToCollPath from last down to index 1 (skip yield token at 0), // then append MOET. e.g. [FUSDEV, PYUSD0, WETH, WBTC] → [WBTC, WETH, PYUSD0] + MOET var collToDebtPath: [EVM.EVMAddress] = [] @@ -1072,7 +1072,7 @@ access(all) contract FlowYieldVaultsStrategiesV2 { // collateral EVM address = last element of yieldToCollateral path // underlying (PYUSD0) EVM address = second element of yieldToCollateral path - assert(yieldToCollPath.length >= 2, message: "yieldToCollateral path must have at least 2 elements") + assert(yieldToCollPath.length >= 2, message: "yieldToCollateral path requires at least yield and collateral tokens, got \(yieldToCollPath.length)") let collateralEVMAddress = yieldToCollPath[yieldToCollPath.length - 1] let underlyingEVMAddress = tokens.underlying4626AssetEVMAddress From 5d46a11289649bc9c9eef17b74d61c4b06ceb600 Mon Sep 17 00:00:00 2001 From: Alex <12097569+nialexsan@users.noreply.github.com> Date: Wed, 25 Mar 2026 17:37:24 -0400 Subject: [PATCH 23/29] fix typo --- cadence/contracts/FlowYieldVaultsStrategiesV2.cdc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cadence/contracts/FlowYieldVaultsStrategiesV2.cdc b/cadence/contracts/FlowYieldVaultsStrategiesV2.cdc index 026c3fc4..59874b02 100644 --- a/cadence/contracts/FlowYieldVaultsStrategiesV2.cdc +++ b/cadence/contracts/FlowYieldVaultsStrategiesV2.cdc @@ -650,7 +650,7 @@ access(all) contract FlowYieldVaultsStrategiesV2 { access(self) fun _initAutoBalancerAndIO( oracle: {DeFiActions.PriceOracle}, yieldTokenType: Type, - recurringConfig: DeFiActions.AutoBalancerRecurringConfig?, + recurringConfig: AutoBalancers.AutoBalancerRecurringConfig?, uniqueID: DeFiActions.UniqueIdentifier ): FlowYieldVaultsStrategiesV2.AutoBalancerIO { let autoBalancerRef = From f84210e69db0900a438717ee603a2abb68606932 Mon Sep 17 00:00:00 2001 From: Alex <12097569+nialexsan@users.noreply.github.com> Date: Wed, 25 Mar 2026 20:06:31 -0400 Subject: [PATCH 24/29] add missing contract deployment --- cadence/tests/FlowYieldVaultsStrategiesV2_FUSDEV_test.cdc | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/cadence/tests/FlowYieldVaultsStrategiesV2_FUSDEV_test.cdc b/cadence/tests/FlowYieldVaultsStrategiesV2_FUSDEV_test.cdc index 404b6113..e123ac36 100644 --- a/cadence/tests/FlowYieldVaultsStrategiesV2_FUSDEV_test.cdc +++ b/cadence/tests/FlowYieldVaultsStrategiesV2_FUSDEV_test.cdc @@ -193,6 +193,14 @@ access(all) fun setup() { ) Test.expect(err, Test.beNil()) + log("Deploying FlowYieldVaultsAutoBalancers...") + err = Test.deployContract( + name: "AutoBalancers", + path: "../../cadence/contracts/AutoBalancers.cdc", + arguments: [] + ) + Test.expect(err, Test.beNil()) + log("Deploying FlowYieldVaultsAutoBalancers...") err = Test.deployContract( name: "FlowYieldVaultsAutoBalancers", From f68b12dedca140c57e4a003a7f0e7a03572f4b69 Mon Sep 17 00:00:00 2001 From: Alex <12097569+nialexsan@users.noreply.github.com> Date: Thu, 26 Mar 2026 12:56:17 -0400 Subject: [PATCH 25/29] backport changes from deployment branch --- .../contracts/FlowYieldVaultsStrategiesV2.cdc | 35 ++++++++++++++----- 1 file changed, 27 insertions(+), 8 deletions(-) diff --git a/cadence/contracts/FlowYieldVaultsStrategiesV2.cdc b/cadence/contracts/FlowYieldVaultsStrategiesV2.cdc index b78afe60..56ea6473 100644 --- a/cadence/contracts/FlowYieldVaultsStrategiesV2.cdc +++ b/cadence/contracts/FlowYieldVaultsStrategiesV2.cdc @@ -65,6 +65,7 @@ access(all) contract FlowYieldVaultsStrategiesV2 { swapperType: String ) + // @TODO rename to UnconstrainedSwapSource or UnboundedSwapSource /// A Source that converts yield tokens to debt tokens by pulling ALL available yield /// tokens from the wrapped source, rather than using quoteIn to limit the pull amount. /// @@ -312,18 +313,15 @@ access(all) contract FlowYieldVaultsStrategiesV2 { uniqueID: self.uniqueID! ) let shortfall = totalDebtAmount - expectedMOET - // Over-deposit by 1% so the remaining debt lands below expectedMOET, giving - // BufferedSwapSource enough margin to cover ERC4626 floor-rounding at redemption - let buffered = shortfall + shortfall / 100.0 - let quote = collateralToMoetSwapper.quoteIn(forDesired: buffered, reverse: false) + let quote = collateralToMoetSwapper.quoteIn(forDesired: shortfall, reverse: false) assert(quote.inAmount > 0.0, message: "Pre-supplement: collateral→MOET quote returned zero input for non-zero shortfall — swapper misconfigured") let extraCollateral <- self.source.withdrawAvailable(maxAmount: quote.inAmount) assert(extraCollateral.balance > 0.0, message: "Pre-supplement: no collateral available to cover shortfall of \(shortfall) MOET") let extraMOET <- collateralToMoetSwapper.swap(quote: quote, inVault: <-extraCollateral) - assert(extraMOET.balance > 0.0, - message: "Pre-supplement: collateral→MOET swap produced zero output") + assert(extraMOET.balance >= shortfall, + message: "Pre-supplement: collateral→MOET swap produced less than shortfall: got \(extraMOET.balance), need \(shortfall)") self.position.deposit(from: <-extraMOET) } @@ -576,8 +574,29 @@ access(all) contract FlowYieldVaultsStrategiesV2 { } } + // @deprecated /// Returned bundle for stored AutoBalancer interactions (reference + caps) access(all) struct AutoBalancerIO { + access(all) let autoBalancer: + auth(DeFiActions.Auto, DeFiActions.Set, DeFiActions.Get, DeFiActions.Schedule, FungibleToken.Withdraw) + &DeFiActions.AutoBalancer + + access(all) let sink: {DeFiActions.Sink} + access(all) let source: {DeFiActions.Source} + + init( + autoBalancer: auth(DeFiActions.Auto, DeFiActions.Set, DeFiActions.Get, DeFiActions.Schedule, FungibleToken.Withdraw) &DeFiActions.AutoBalancer, + sink: {DeFiActions.Sink}, + source: {DeFiActions.Source} + ) { + self.sink = sink + self.source = source + self.autoBalancer = autoBalancer + } + } + + /// Returned bundle for stored AutoBalancer interactions (reference + caps) + access(all) struct AutoBalancerIO_v2 { access(all) let autoBalancer: auth(AutoBalancers.Auto, AutoBalancers.Set, AutoBalancers.Get, AutoBalancers.Schedule, FungibleToken.Withdraw) &AutoBalancers.AutoBalancer @@ -652,7 +671,7 @@ access(all) contract FlowYieldVaultsStrategiesV2 { yieldTokenType: Type, recurringConfig: AutoBalancers.AutoBalancerRecurringConfig?, uniqueID: DeFiActions.UniqueIdentifier - ): FlowYieldVaultsStrategiesV2.AutoBalancerIO { + ): FlowYieldVaultsStrategiesV2.AutoBalancerIO_v2 { let autoBalancerRef = FlowYieldVaultsAutoBalancersV1._initNewAutoBalancer( oracle: oracle, @@ -670,7 +689,7 @@ access(all) contract FlowYieldVaultsStrategiesV2 { let source = autoBalancerRef.createBalancerSource() ?? panic("Could not retrieve Source from AutoBalancer with id \(uniqueID.id)") - return FlowYieldVaultsStrategiesV2.AutoBalancerIO( + return FlowYieldVaultsStrategiesV2.AutoBalancerIO_v2( autoBalancer: autoBalancerRef, sink: sink, source: source From 5f2104f599a754a8bbf22a7dd6c3dc573e01d0fb Mon Sep 17 00:00:00 2001 From: Alex <12097569+nialexsan@users.noreply.github.com> Date: Thu, 26 Mar 2026 15:08:22 -0400 Subject: [PATCH 26/29] balance assertion --- ...lowYieldVaultsStrategiesV2_FUSDEV_test.cdc | 27 ++++++++++++++++--- 1 file changed, 24 insertions(+), 3 deletions(-) diff --git a/cadence/tests/FlowYieldVaultsStrategiesV2_FUSDEV_test.cdc b/cadence/tests/FlowYieldVaultsStrategiesV2_FUSDEV_test.cdc index e123ac36..df0b7d68 100644 --- a/cadence/tests/FlowYieldVaultsStrategiesV2_FUSDEV_test.cdc +++ b/cadence/tests/FlowYieldVaultsStrategiesV2_FUSDEV_test.cdc @@ -389,6 +389,7 @@ access(all) fun testWithdrawFromFUSDEVYieldVault_WFLOW() { access(all) fun testCloseFUSDEVYieldVault_WFLOW() { let vaultBalBefore = (_executeScript("../scripts/flow-yield-vaults/get_yield_vault_balance.cdc", [flowUser.address, flowVaultID]).returnValue! as! UFix64?) ?? 0.0 + let collateralBefore = (_executeScript("../scripts/flow-yield-vaults/get_flow_balance.cdc", [flowUser.address]).returnValue! as! UFix64) log("Closing WFLOW vault ".concat(flowVaultID.toString()).concat(" (balance: ").concat(vaultBalBefore.toString()).concat(")...")) Test.expect( _executeTransactionFile("../transactions/flow-yield-vaults/close_yield_vault.cdc", [flowVaultID], [flowUser]), @@ -397,7 +398,13 @@ access(all) fun testCloseFUSDEVYieldVault_WFLOW() { let vaultBalAfter = _executeScript("../scripts/flow-yield-vaults/get_yield_vault_balance.cdc", [flowUser.address, flowVaultID]) Test.expect(vaultBalAfter, Test.beSucceeded()) Test.assert(vaultBalAfter.returnValue == nil, message: "WFLOW vault should no longer exist after close") - log("WFLOW yield vault closed successfully") + let collateralAfter = (_executeScript("../scripts/flow-yield-vaults/get_flow_balance.cdc", [flowUser.address]).returnValue! as! UFix64) + // After close the debt is fully repaid (closePosition would have reverted otherwise). + // Assert that the collateral returned is within 5% of the vault NAV before close, + // accounting for UniV3 swap fees and any pre-supplement collateral sold to cover shortfall. + Test.assert(equalAmounts(a: collateralAfter, b: collateralBefore + vaultBalBefore, tolerance: vaultBalBefore / 20.0), + message: "WFLOW close: expected ~".concat(vaultBalBefore.toString()).concat(" FLOW returned, collateralBefore=").concat(collateralBefore.toString()).concat(" collateralAfter=").concat(collateralAfter.toString())) + log("WFLOW yield vault closed successfully, collateral returned: ".concat(collateralAfter.toString())) } /* ========================================================= @@ -452,7 +459,9 @@ access(all) fun testWithdrawFromFUSDEVYieldVault_WBTC() { } access(all) fun testCloseFUSDEVYieldVault_WBTC() { + let wbtcBalancePath: PublicPath = /public/EVMVMBridgedToken_717dae2baf7656be9a9b01dee31d571a9d4c9579Receiver let vaultBalBefore = (_executeScript("../scripts/flow-yield-vaults/get_yield_vault_balance.cdc", [wbtcUser.address, wbtcVaultID]).returnValue! as! UFix64?) ?? 0.0 + let collateralBefore = (_executeScript("../scripts/tokens/get_balance.cdc", [wbtcUser.address, wbtcBalancePath]).returnValue! as! UFix64?) ?? 0.0 log("Closing WBTC vault ".concat(wbtcVaultID.toString()).concat(" (balance: ").concat(vaultBalBefore.toString()).concat(")...")) Test.expect( _executeTransactionFile("../transactions/flow-yield-vaults/close_yield_vault.cdc", [wbtcVaultID], [wbtcUser]), @@ -461,7 +470,12 @@ access(all) fun testCloseFUSDEVYieldVault_WBTC() { let vaultBalAfter = _executeScript("../scripts/flow-yield-vaults/get_yield_vault_balance.cdc", [wbtcUser.address, wbtcVaultID]) Test.expect(vaultBalAfter, Test.beSucceeded()) Test.assert(vaultBalAfter.returnValue == nil, message: "WBTC vault should no longer exist after close") - log("WBTC yield vault closed successfully") + let collateralAfter = (_executeScript("../scripts/tokens/get_balance.cdc", [wbtcUser.address, wbtcBalancePath]).returnValue! as! UFix64?) ?? 0.0 + // After close the debt is fully repaid (closePosition would have reverted otherwise). + // Assert that the collateral returned is within 5% of the vault NAV before close. + Test.assert(equalAmounts(a: collateralAfter, b: collateralBefore + vaultBalBefore, tolerance: vaultBalBefore / 20.0), + message: "WBTC close: expected ~".concat(vaultBalBefore.toString()).concat(" WBTC returned, collateralBefore=").concat(collateralBefore.toString()).concat(" collateralAfter=").concat(collateralAfter.toString())) + log("WBTC yield vault closed successfully, collateral returned: ".concat(collateralAfter.toString())) } /* ========================================================= @@ -516,7 +530,9 @@ access(all) fun testWithdrawFromFUSDEVYieldVault_WETH() { } access(all) fun testCloseFUSDEVYieldVault_WETH() { + let wethBalancePath: PublicPath = /public/EVMVMBridgedToken_2f6f07cdcf3588944bf4c42ac74ff24bf56e7590Receiver let vaultBalBefore = (_executeScript("../scripts/flow-yield-vaults/get_yield_vault_balance.cdc", [wethUser.address, wethVaultID]).returnValue! as! UFix64?) ?? 0.0 + let collateralBefore = (_executeScript("../scripts/tokens/get_balance.cdc", [wethUser.address, wethBalancePath]).returnValue! as! UFix64?) ?? 0.0 log("Closing WETH vault ".concat(wethVaultID.toString()).concat(" (balance: ").concat(vaultBalBefore.toString()).concat(")...")) Test.expect( _executeTransactionFile("../transactions/flow-yield-vaults/close_yield_vault.cdc", [wethVaultID], [wethUser]), @@ -525,7 +541,12 @@ access(all) fun testCloseFUSDEVYieldVault_WETH() { let vaultBalAfter = _executeScript("../scripts/flow-yield-vaults/get_yield_vault_balance.cdc", [wethUser.address, wethVaultID]) Test.expect(vaultBalAfter, Test.beSucceeded()) Test.assert(vaultBalAfter.returnValue == nil, message: "WETH vault should no longer exist after close") - log("WETH yield vault closed successfully") + let collateralAfter = (_executeScript("../scripts/tokens/get_balance.cdc", [wethUser.address, wethBalancePath]).returnValue! as! UFix64?) ?? 0.0 + // After close the debt is fully repaid (closePosition would have reverted otherwise). + // Assert that the collateral returned is within 5% of the vault NAV before close. + Test.assert(equalAmounts(a: collateralAfter, b: collateralBefore + vaultBalBefore, tolerance: vaultBalBefore / 20.0), + message: "WETH close: expected ~".concat(vaultBalBefore.toString()).concat(" WETH returned, collateralBefore=").concat(collateralBefore.toString()).concat(" collateralAfter=").concat(collateralAfter.toString())) + log("WETH yield vault closed successfully, collateral returned: ".concat(collateralAfter.toString())) } /* ========================================================= From 2f98aeb1a6f5d585ee837f32adddc225dbaa3f0b Mon Sep 17 00:00:00 2001 From: Alex <12097569+nialexsan@users.noreply.github.com> Date: Fri, 27 Mar 2026 11:34:16 -0400 Subject: [PATCH 27/29] deprecate buffered swapper --- .../contracts/FlowYieldVaultsStrategiesV2.cdc | 59 +++---------------- 1 file changed, 9 insertions(+), 50 deletions(-) diff --git a/cadence/contracts/FlowYieldVaultsStrategiesV2.cdc b/cadence/contracts/FlowYieldVaultsStrategiesV2.cdc index 56ea6473..420a5cbb 100644 --- a/cadence/contracts/FlowYieldVaultsStrategiesV2.cdc +++ b/cadence/contracts/FlowYieldVaultsStrategiesV2.cdc @@ -65,17 +65,8 @@ access(all) contract FlowYieldVaultsStrategiesV2 { swapperType: String ) - // @TODO rename to UnconstrainedSwapSource or UnboundedSwapSource - /// A Source that converts yield tokens to debt tokens by pulling ALL available yield - /// tokens from the wrapped source, rather than using quoteIn to limit the pull amount. - /// - /// This avoids ERC4626 rounding issues where quoteIn might underestimate required shares, - /// causing the swap to return less than the requested debt amount. By pulling everything - /// and swapping everything, the output is as large as the yield position allows. - /// - /// The caller is responsible for ensuring the yield tokens (after swapping) will cover the - /// required debt — e.g. by pre-depositing supplemental MOET to reduce the position's debt - /// before calling closePosition (see FUSDEVStrategy.closePosition step 6). + /// Deprecated — replaced by SwapConnectors.SwapSource. Kept as a no-op to preserve + /// contract upgrade compatibility (Cadence structs cannot be removed once deployed). access(all) struct BufferedSwapSource : DeFiActions.Source { access(self) let swapper: {DeFiActions.Swapper} access(self) let source: {DeFiActions.Source} @@ -86,52 +77,20 @@ access(all) contract FlowYieldVaultsStrategiesV2 { source: {DeFiActions.Source}, uniqueID: DeFiActions.UniqueIdentifier? ) { - pre { - source.getSourceType() == swapper.inType(): - "source type != swapper inType" - } self.swapper = swapper self.source = source self.uniqueID = uniqueID } access(all) fun getComponentInfo(): DeFiActions.ComponentInfo { - return DeFiActions.ComponentInfo( - type: self.getType(), - id: self.id(), - innerComponents: [ - self.swapper.getComponentInfo(), - self.source.getComponentInfo() - ] - ) + return DeFiActions.ComponentInfo(type: self.getType(), id: self.id(), innerComponents: []) } access(contract) view fun copyID(): DeFiActions.UniqueIdentifier? { return self.uniqueID } access(contract) fun setID(_ id: DeFiActions.UniqueIdentifier?) { self.uniqueID = id } access(all) view fun getSourceType(): Type { return self.swapper.outType() } - access(all) fun minimumAvailable(): UFix64 { - let avail = self.source.minimumAvailable() - if avail == 0.0 { return 0.0 } - return self.swapper.quoteOut(forProvided: avail, reverse: false).outAmount - } - /// Pulls ALL available yield tokens from the source and swaps them to the debt token. - /// Ignores quoteIn — avoids ERC4626 rounding underestimates that would leave us short. + access(all) fun minimumAvailable(): UFix64 { return 0.0 } access(FungibleToken.Withdraw) fun withdrawAvailable(maxAmount: UFix64): @{FungibleToken.Vault} { - if maxAmount == 0.0 { - return <- DeFiActionsUtils.getEmptyVault(self.getSourceType()) - } - let availableIn = self.source.minimumAvailable() - if availableIn == 0.0 { - return <- DeFiActionsUtils.getEmptyVault(self.getSourceType()) - } - // Pull ALL available yield tokens (not quoteIn-limited) - let sourceLiquidity <- self.source.withdrawAvailable(maxAmount: availableIn) - if sourceLiquidity.balance == 0.0 { - Burner.burn(<-sourceLiquidity) - return <- DeFiActionsUtils.getEmptyVault(self.getSourceType()) - } - let swapped <- self.swapper.swap(quote: nil, inVault: <-sourceLiquidity) - assert(swapped.balance > 0.0, message: "BufferedSwapSource: swap returned zero despite available input") - return <- swapped + return <- DeFiActionsUtils.getEmptyVault(self.getSourceType()) } } @@ -325,10 +284,10 @@ access(all) contract FlowYieldVaultsStrategiesV2 { self.position.deposit(from: <-extraMOET) } - // Step 7: Create a BufferedSwapSource that converts ALL yield tokens → MOET. - // Pulling all (not quoteIn-limited) avoids ERC4626 rounding underestimates. - // After the pre-supplement above, the remaining debt is covered by the yield tokens. - let moetSource = FlowYieldVaultsStrategiesV2.BufferedSwapSource( + // Step 7: Create a SwapSource that converts yield tokens → MOET for debt repayment. + // Step 6's pre-supplement ensures remaining debt ≤ yield value, so SwapSource will + // use quoteIn(remainingDebt) and pull only the shares needed — not the full balance. + let moetSource = SwapConnectors.SwapSource( swapper: yieldToMoetSwapper, source: yieldTokenSource, uniqueID: self.copyID() From 462d3cb19c2a86b11f0bea02a076cfebe3fcdb6a Mon Sep 17 00:00:00 2001 From: Alex <12097569+nialexsan@users.noreply.github.com> Date: Mon, 30 Mar 2026 16:06:25 -0400 Subject: [PATCH 28/29] return excess yield tokens --- .../contracts/FlowYieldVaultsStrategiesV2.cdc | 42 +++++++++++++++++++ 1 file changed, 42 insertions(+) diff --git a/cadence/contracts/FlowYieldVaultsStrategiesV2.cdc b/cadence/contracts/FlowYieldVaultsStrategiesV2.cdc index 420a5cbb..a6d7838c 100644 --- a/cadence/contracts/FlowYieldVaultsStrategiesV2.cdc +++ b/cadence/contracts/FlowYieldVaultsStrategiesV2.cdc @@ -339,6 +339,48 @@ access(all) contract FlowYieldVaultsStrategiesV2 { } destroy resultVaults + + // Step 9: Drain any remaining FUSDEV shares from the AutoBalancer — excess yield + // not consumed during debt repayment — and convert them directly to collateral. + // The SwapSource inside closePosition only pulled what was needed to repay the debt; + // any surplus shares are still held by the AutoBalancer and are recovered here. + let excessShares <- yieldTokenSource.withdrawAvailable(maxAmount: UFix64.max) + if excessShares.balance > 0.0 { + let moetQuote = yieldToMoetSwapper.quoteOut(forProvided: excessShares.balance, reverse: false) + if moetQuote.outAmount > 0.0 { + let moetVault <- yieldToMoetSwapper.swap(quote: moetQuote, inVault: <-excessShares) + let collQuote = debtToCollateralSwapper.quoteOut(forProvided: moetVault.balance, reverse: false) + if collQuote.outAmount > 0.0 { + let extraCollateral <- debtToCollateralSwapper.swap(quote: collQuote, inVault: <-moetVault) + collateralVault.deposit(from: <-extraCollateral) + } else { + emit DustBurned( + tokenType: moetVault.getType().identifier, + balance: moetVault.balance, + quoteInType: collQuote.inType.identifier, + quoteOutType: collQuote.outType.identifier, + quoteInAmount: collQuote.inAmount, + quoteOutAmount: collQuote.outAmount, + swapperType: debtToCollateralSwapper.getType().identifier + ) + Burner.burn(<-moetVault) + } + } else { + emit DustBurned( + tokenType: excessShares.getType().identifier, + balance: excessShares.balance, + quoteInType: moetQuote.inType.identifier, + quoteOutType: moetQuote.outType.identifier, + quoteInAmount: moetQuote.inAmount, + quoteOutAmount: moetQuote.outAmount, + swapperType: yieldToMoetSwapper.getType().identifier + ) + Burner.burn(<-excessShares) + } + } else { + Burner.burn(<-excessShares) + } + self._markPositionClosed() return <- collateralVault } From d0e844b6d143deff358dbed80cff3e5e6026561e Mon Sep 17 00:00:00 2001 From: Alex <12097569+nialexsan@users.noreply.github.com> Date: Mon, 30 Mar 2026 16:35:41 -0400 Subject: [PATCH 29/29] Update cadence/contracts/FlowYieldVaultsStrategiesV2.cdc Co-authored-by: Leo Zhang --- cadence/contracts/FlowYieldVaultsStrategiesV2.cdc | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/cadence/contracts/FlowYieldVaultsStrategiesV2.cdc b/cadence/contracts/FlowYieldVaultsStrategiesV2.cdc index a6d7838c..edaf680f 100644 --- a/cadence/contracts/FlowYieldVaultsStrategiesV2.cdc +++ b/cadence/contracts/FlowYieldVaultsStrategiesV2.cdc @@ -421,7 +421,9 @@ access(all) contract FlowYieldVaultsStrategiesV2 { return issuer!.getCollateralConfig(strategyType: strategyType, collateralType: collateralType) } - /// Builds a YIELD→MOET MultiSwapper (AMM direct + ERC4626 redeem path). + /// Builds a YIELD→MOET MultiSwapper that contains 2 paths (AMM direct + ERC4626 redeem path). + // AMM direct path: YIELD (FUSDEV) -> MOET + // ERC4626 redeem path: YIELD (FUSDEV) -> PYUSD0 -> MOET access(self) fun _buildYieldToDebtSwapper( tokens: FlowYieldVaultsStrategiesV2.TokenBundle, uniqueID: DeFiActions.UniqueIdentifier