diff --git a/cadence/contracts/FlowYieldVaultsStrategiesV2.cdc b/cadence/contracts/FlowYieldVaultsStrategiesV2.cdc index edaf680f..d2ff54a8 100644 --- a/cadence/contracts/FlowYieldVaultsStrategiesV2.cdc +++ b/cadence/contracts/FlowYieldVaultsStrategiesV2.cdc @@ -20,8 +20,6 @@ import "FlowYieldVaults" import "FlowYieldVaultsAutoBalancersV1" // scheduler import "FlowTransactionScheduler" -// tokens -import "MOET" // vm bridge import "FlowEVMBridgeConfig" // live oracles @@ -47,6 +45,7 @@ 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: /// "closedPositions" → {UInt64: Bool} + /// "moreERC4626Configs" → {Type: {Type: {Type: MoreERC4626CollateralConfig}}} access(contract) let config: {String: AnyStruct} /// Canonical StoragePath where the StrategyComposerIssuer should be stored @@ -119,10 +118,77 @@ access(all) contract FlowYieldVaultsStrategiesV2 { } } + /// @deprecated — no longer used. Retained for Cadence upgrade compatibility (structs cannot + /// be removed once deployed on-chain). + access(all) struct MoetPreswapConfig { + access(all) let collateralToMoetAddressPath: [EVM.EVMAddress] + 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] + ) { + pre { + yieldToUnderlyingUniV3AddressPath.length > 1: + "Invalid yieldToUnderlying UniV3 path length" + yieldToUnderlyingUniV3FeePath.length == yieldToUnderlyingUniV3AddressPath.length - 1: + "Invalid yieldToUnderlying UniV3 fee path length" + yieldToUnderlyingUniV3AddressPath[0].equals(yieldTokenEVMAddress): + "yieldToUnderlying UniV3 path must start with yield token" + debtToCollateralUniV3AddressPath.length > 1: + "Invalid debtToCollateral UniV3 path length" + debtToCollateralUniV3FeePath.length == debtToCollateralUniV3AddressPath.length - 1: + "Invalid debtToCollateral UniV3 fee path length" + debtToCollateralUniV3AddressPath[0].equals(yieldToUnderlyingUniV3AddressPath[yieldToUnderlyingUniV3AddressPath.length - 1]): + "debtToCollateral UniV3 path must start with the underlying asset (end of yieldToUnderlying path)" + } + 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. - /// Each strategy instance holds exactly one collateral type and one debt type (MOET). + /// Deposits collateral into a single FlowALP position, borrowing PYUSD0 as debt. + /// PYUSD0 is deposited directly into the Morpho FUSDEV ERC4626 vault (no AMM swap needed + /// since PYUSD0 is the vault's underlying asset). + /// Each strategy instance holds exactly one collateral type and one debt type (PYUSD0). /// PYUSD0 (the FUSDEV vault's underlying asset) cannot be used as collateral. access(all) resource FUSDEVStrategy : FlowYieldVaults.Strategy, DeFiActions.IdentifiableResource { /// An optional identifier allowing protocols to identify stacked connector operations by defining a protocol- @@ -175,8 +241,8 @@ access(all) contract FlowYieldVaultsStrategiesV2 { /// This method: /// 1. Calculates debt amount from position /// 2. Creates external yield token source from AutoBalancer - /// 3. Swaps yield tokens → MOET via stored swapper - /// 4. Closes position with prepared MOET vault + /// 3. Swaps yield tokens → PYUSD0 via stored swapper + /// 4. Closes position with prepared PYUSD0 vault /// /// This approach eliminates circular dependencies by preparing all funds externally /// before calling the position's close method. @@ -233,7 +299,7 @@ access(all) contract FlowYieldVaultsStrategiesV2 { let yieldTokenSource = FlowYieldVaultsAutoBalancersV1.createExternalSource(id: self.id()!) ?? panic("Could not create external source from AutoBalancer") - // Step 5: Reconstruct yield→MOET swapper from stored CollateralConfig. + // Step 5: Reconstruct yield→PYUSD0 swapper from stored CollateralConfig. let closeCollateralConfig = self._getStoredCollateralConfig( strategyType: Type<@FUSDEVStrategy>(), collateralType: collateralType @@ -241,67 +307,67 @@ access(all) contract FlowYieldVaultsStrategiesV2 { let closeTokens = FlowYieldVaultsStrategiesV2._resolveTokenBundle( yieldTokenEVMAddress: closeCollateralConfig.yieldTokenEVMAddress ) - let yieldToMoetSwapper = self._buildYieldToDebtSwapper( + let yieldToPyusd0Swapper = self._buildYieldToDebtSwapper( tokens: closeTokens, uniqueID: self.uniqueID! ) // 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. + // The FUSDEV close path has a negligible round-trip fee: + // Open: PYUSD0 → FUSDEV (ERC4626 deposit, free) + // Close: FUSDEV → PYUSD0 (ERC4626 redeem, free) + // In production, accrued yield more than covers any rounding; with no accrued yield + // (e.g. in tests, immediate open+close), the yield tokens may convert back to slightly + // less PYUSD0 than was borrowed. We handle this by pre-pulling a tiny amount of + // collateral from self.source, swapping it to PYUSD0, 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 + let expectedPyusd0 = yieldAvail > 0.0 + ? yieldToPyusd0Swapper.quoteOut(forProvided: yieldAvail, reverse: false).outAmount : 0.0 - if expectedMOET < totalDebtAmount { - let collateralToMoetSwapper = self._buildCollateralToDebtSwapper( + if expectedPyusd0 < totalDebtAmount { + let collateralToPyusd0Swapper = self._buildCollateralToDebtSwapper( collateralConfig: closeCollateralConfig, tokens: closeTokens, collateralType: collateralType, uniqueID: self.uniqueID! ) - let shortfall = totalDebtAmount - expectedMOET - let quote = collateralToMoetSwapper.quoteIn(forDesired: shortfall, reverse: false) + let shortfall = totalDebtAmount - expectedPyusd0 + let quote = collateralToPyusd0Swapper.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") + message: "Pre-supplement: collateral→PYUSD0 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 >= shortfall, - message: "Pre-supplement: collateral→MOET swap produced less than shortfall: got \(extraMOET.balance), need \(shortfall)") - self.position.deposit(from: <-extraMOET) + message: "Pre-supplement: no collateral available to cover shortfall of \(shortfall) PYUSD0") + let extraPyusd0 <- collateralToPyusd0Swapper.swap(quote: quote, inVault: <-extraCollateral) + assert(extraPyusd0.balance >= shortfall, + message: "Pre-supplement: collateral→PYUSD0 swap produced less than shortfall: got \(extraPyusd0.balance), need \(shortfall)") + self.position.deposit(from: <-extraPyusd0) } - // Step 7: Create a SwapSource that converts yield tokens → MOET for debt repayment. + // Step 7: Create a SwapSource that converts yield tokens → PYUSD0 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, + let debtSource = SwapConnectors.SwapSource( + swapper: yieldToPyusd0Swapper, source: yieldTokenSource, uniqueID: self.copyID() ) - // Step 8: Close position - pool pulls up to the (now pre-reduced) debt from moetSource - let resultVaults <- self.position.closePosition(repaymentSources: [moetSource]) + // Step 8: Close position - pool pulls up to the (now pre-reduced) debt from debtSource + let resultVaults <- self.position.closePosition(repaymentSources: [debtSource]) // 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. + // the collateral vault and optionally a PYUSD0 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 reconstructed swapper. - // Reconstruct MOET→YIELD→collateral from CollateralConfig. + // vaults (PYUSD0 overpayment dust) back to collateral via reconstructed swapper. + // Reconstruct PYUSD0→collateral path from CollateralConfig. let debtToCollateralSwapper = self._buildDebtToCollateralSwapper( collateralConfig: closeCollateralConfig, tokens: closeTokens, @@ -344,36 +410,43 @@ access(all) contract FlowYieldVaultsStrategiesV2 { // 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. + // + // Use a MultiSwapper so the best available route is chosen: + // - Direct: FUSDEV → collateral via the stored yieldToCollateral AMM path (works + // even for 2-element paths where a direct yield↔collateral pool exists) + // - 2-hop: FUSDEV → PYUSD0 → collateral (via yieldToPyusd0 + debtToCollateral) 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) - } + let yieldToCollateralDirect = FlowYieldVaultsStrategiesV2._buildUniV3Swapper( + tokenPath: closeCollateralConfig.yieldToCollateralUniV3AddressPath, + feePath: closeCollateralConfig.yieldToCollateralUniV3FeePath, + inVault: closeTokens.yieldTokenType, + outVault: collateralType, + uniqueID: self.uniqueID! + ) + let yieldToCollateralViaDebt = SwapConnectors.SequentialSwapper( + swappers: [yieldToPyusd0Swapper, debtToCollateralSwapper], + uniqueID: self.copyID() + ) + let sharesToCollateral = SwapConnectors.MultiSwapper( + inVault: closeTokens.yieldTokenType, + outVault: collateralType, + swappers: [yieldToCollateralDirect, yieldToCollateralViaDebt], + uniqueID: self.copyID() + ) + let quote = sharesToCollateral.quoteOut(forProvided: excessShares.balance, reverse: false) + if quote.outAmount > 0.0 { + let extraCollateral <- sharesToCollateral.swap(quote: quote, inVault: <-excessShares) + collateralVault.deposit(from: <-extraCollateral) } 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 + quoteInType: quote.inType.identifier, + quoteOutType: quote.outType.identifier, + quoteInAmount: quote.inAmount, + quoteOutAmount: quote.outAmount, + swapperType: sharesToCollateral.getType().identifier ) Burner.burn(<-excessShares) } @@ -421,20 +494,21 @@ access(all) contract FlowYieldVaultsStrategiesV2 { return issuer!.getCollateralConfig(strategyType: strategyType, collateralType: collateralType) } - /// 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 + /// Builds a YIELD→PYUSD0 MultiSwapper (AMM direct + ERC4626 redeem path). + /// PYUSD0 is the underlying asset of the FUSDEV vault and is also the debt token. access(self) fun _buildYieldToDebtSwapper( tokens: FlowYieldVaultsStrategiesV2.TokenBundle, uniqueID: DeFiActions.UniqueIdentifier ): SwapConnectors.MultiSwapper { + // Direct FUSDEV→PYUSD0 via AMM (fee 100) let yieldToDebtAMM = FlowYieldVaultsStrategiesV2._buildUniV3Swapper( - tokenPath: [tokens.yieldTokenEVMAddress, tokens.moetTokenEVMAddress], + tokenPath: [tokens.yieldTokenEVMAddress, tokens.underlying4626AssetEVMAddress], feePath: [100], inVault: tokens.yieldTokenType, - outVault: tokens.moetTokenType, + outVault: tokens.underlying4626AssetType, uniqueID: uniqueID ) + // FUSDEV→PYUSD0 via Morpho ERC4626 redeem (no additional swap needed) let yieldToUnderlying = MorphoERC4626SwapConnectors.Swapper( vaultEVMAddress: tokens.yieldTokenEVMAddress, coa: FlowYieldVaultsStrategiesV2._getCOACapability(), @@ -442,29 +516,18 @@ access(all) contract FlowYieldVaultsStrategiesV2 { 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], + outVault: tokens.underlying4626AssetType, + swappers: [yieldToDebtAMM, yieldToUnderlying], uniqueID: uniqueID ) } - /// Builds a collateral→MOET UniV3 swapper from CollateralConfig. + /// Builds a collateral→PYUSD0 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] + /// yield token); PYUSD0 is the underlying asset and the debt token, so no further hop needed. + /// e.g. [FUSDEV, PYUSD0, WETH, WBTC] → [WBTC, WETH, PYUSD0] access(self) fun _buildCollateralToDebtSwapper( collateralConfig: FlowYieldVaultsStrategiesV2.CollateralConfig, tokens: FlowYieldVaultsStrategiesV2.TokenBundle, @@ -473,55 +536,65 @@ access(all) contract FlowYieldVaultsStrategiesV2 { ): UniswapV3SwapConnectors.Swapper { let yieldToCollPath = collateralConfig.yieldToCollateralUniV3AddressPath let yieldToCollFees = collateralConfig.yieldToCollateralUniV3FeePath - 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 + // Requires at least 3 elements: [yield, PYUSD0 (debt/underlying), collateral, ...]. + // PYUSD0 must be at index 1 — it is the debt token and the derivation logic below + // assumes it as the starting point of the collateral→debt path. A direct yield↔collateral + // pool (2-element path) would omit PYUSD0 entirely and is structurally incompatible. + assert(yieldToCollPath.length >= 3, message: "yieldToCollateral path must have at least 3 elements [yield, PYUSD0, collateral] — a direct yield↔collateral pool is incompatible with FUSDEVStrategy debt routing") + // Build reversed path: iterate yieldToCollPath from last down to index 1 (skip yield token at 0). + // e.g. [FUSDEV, PYUSD0, WETH, WBTC] → [WBTC, WETH, PYUSD0] 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 + // Build reversed fees: iterate from last down to index 1 (skip yield→underlying fee at 0). + // e.g. [100, 3000, 3000] → [3000, 3000] for i in InclusiveRange(yieldToCollFees.length - 1, 1, step: -1) { collToDebtFees.append(yieldToCollFees[i]) } - collToDebtFees.append(UInt32(100)) return FlowYieldVaultsStrategiesV2._buildUniV3Swapper( tokenPath: collToDebtPath, feePath: collToDebtFees, inVault: collateralType, - outVault: tokens.moetTokenType, + outVault: tokens.underlying4626AssetType, uniqueID: uniqueID ) } - /// Builds a MOET→collateral SequentialSwapper for dust handling: MOET→YIELD→collateral. + /// Builds a PYUSD0→collateral UniV3 swapper for overpayment dust handling. + /// Uses the yieldToCollateral path[1..] (skipping the yield token at index 0), + /// going directly from PYUSD0 (the debt/underlying token) to collateral. + /// e.g. [FUSDEV, PYUSD0, WETH] fees [100, 3000] → [PYUSD0, WETH] fees [3000] 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, + ): UniswapV3SwapConnectors.Swapper { + let path = collateralConfig.yieldToCollateralUniV3AddressPath + let fees = collateralConfig.yieldToCollateralUniV3FeePath + // Requires at least 3 elements: [yield, PYUSD0 (debt/underlying), collateral, ...]. + // PYUSD0 must be at index 1 — it is the debt token and the derivation logic below + // assumes it as the starting point of the debt→collateral path. A direct yield↔collateral + // pool (2-element path) would omit PYUSD0 entirely and is structurally incompatible. + assert(path.length >= 3, message: "yieldToCollateral path must have at least 3 elements [yield, PYUSD0, collateral] — a direct yield↔collateral pool is incompatible with FUSDEVStrategy debt routing") + // Skip the yield token at index 0; path[1..] starts at PYUSD0 (the underlying/debt token). + var pyusd0ToCollPath: [EVM.EVMAddress] = [] + var pyusd0ToCollFees: [UInt32] = [] + for i in InclusiveRange(1, path.length - 1) { + pyusd0ToCollPath.append(path[i]) + } + for i in InclusiveRange(1, fees.length - 1) { + pyusd0ToCollFees.append(fees[i]) + } + return FlowYieldVaultsStrategiesV2._buildUniV3Swapper( + tokenPath: pyusd0ToCollPath, + feePath: pyusd0ToCollFees, + inVault: tokens.underlying4626AssetType, outVault: collateralType, uniqueID: uniqueID ) - return SwapConnectors.SequentialSwapper( - swappers: [debtToYieldAMM, yieldToCollateral], - uniqueID: uniqueID - ) } access(self) view fun _isPositionClosed(): Bool { @@ -549,8 +622,311 @@ 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} + /// Tracks whether the underlying FlowALP position has been closed. + /// NOTE: FUSDEVStrategy stores this flag in the contract-level "closedPositions" config + /// partition (via _isPositionClosed / _markPositionClosed) because FUSDEVStrategy was + /// already deployed on-chain when that tracking was introduced, and Cadence does not allow + /// adding fields to existing deployed resources. syWFLOWvStrategy was added after that + /// point and can therefore carry the flag as a plain resource field. + access(self) var positionClosed: Bool + + init( + id: DeFiActions.UniqueIdentifier, + collateralType: Type, + position: @FlowALPv0.Position + ) { + self.uniqueID = id + self.sink = position.createSink(type: collateralType) + self.source = position.createSourceWithOptions(type: collateralType, pullFromTopUpSource: true) + 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} { + 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 + } + /// Deposits up to the inner Sink's capacity from the provided authorized Vault reference. + /// 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(): + "syWFLOWvStrategy position only accepts \(self.sink.getSinkType().identifier) as collateral, got \(from.getType().identifier)" + } + 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. + access(FungibleToken.Withdraw) fun withdraw(maxAmount: UFix64, ofToken: Type): @{FungibleToken.Vault} { + if ofToken != self.source.getSourceType() { + 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. + 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))" + } + + // 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 + self.positionClosed = true + return <- collateralVault + } + + // Step 3: Reconstruct MoreERC4626CollateralConfig and swappers from contract-level config. + let closeConfig = self._getStoredMoreERC4626Config( + strategyType: Type<@syWFLOWvStrategy>(), + collateralType: collateralType + ) ?? panic("No MoreERC4626CollateralConfig for syWFLOWvStrategy with \(collateralType.identifier)") + let closeTokens = FlowYieldVaultsStrategiesV2._resolveTokenBundle( + yieldTokenEVMAddress: closeConfig.yieldTokenEVMAddress + ) + let syWFLOWvToFlow = self._buildSyWFLOWvToFlowSwapper( + closeConfig: closeConfig, + closeTokens: closeTokens, + uniqueID: self.uniqueID! + ) + let flowToCollateral = self._buildFlowToCollateralSwapper( + closeConfig: closeConfig, + closeTokens: closeTokens, + collateralType: collateralType, + uniqueID: self.uniqueID! + ) + + // Step 4: Create external syWFLOWv source from AutoBalancer + let yieldTokenSource = FlowYieldVaultsAutoBalancersV1.createExternalSource(id: self.id()!) + ?? panic("Could not create external source from AutoBalancer") + + // Step 5: Create a SwapSource that converts syWFLOWv → FLOW for debt repayment. + // SwapSource uses quoteIn when yield value >= debt (pulling only the needed shares), + // or quoteOut when yield is insufficient (pulling everything as a best-effort). + // Any FLOW overpayment is returned as dust and converted back to collateral below. + let flowSource = SwapConnectors.SwapSource( + swapper: syWFLOWvToFlow, + source: yieldTokenSource, + uniqueID: self.copyID() + ) + + // Step 6: Pre-supplement from collateral if yield tokens are insufficient to cover the FLOW debt. + // + // The syWFLOWv close path has a structural round-trip fee loss: + // Open: FLOW → syWFLOWv (ERC4626 deposit, free) + // Close: syWFLOWv → FLOW (UniV3 AMM swap, ~0.3% fee) + // 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 FLOW + // than was borrowed. We handle this by pre-pulling a tiny amount of collateral from + // self.source, swapping it to FLOW via flowToCollateral in reverse, 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 expectedFlow = flowSource.minimumAvailable() + if expectedFlow < totalDebtAmount { + let shortfall = totalDebtAmount - expectedFlow + let quote = flowToCollateral.quoteIn(forDesired: shortfall, reverse: true) + assert(quote.inAmount > 0.0, + message: "Pre-supplement: collateral→FLOW 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) FLOW") + let extraFlow <- flowToCollateral.swapBack(quote: quote, residual: <-extraCollateral) + assert(extraFlow.balance >= shortfall, + message: "Pre-supplement: collateral→FLOW swap produced less than shortfall: got \(extraFlow.balance), need \(shortfall)") + self.position.deposit(from: <-extraFlow) + } + + // Step 7: Close position — pool pulls the (now pre-reduced) FLOW debt from flowSource + let resultVaults <- self.position.closePosition(repaymentSources: [flowSource]) + + // closePosition returns vaults in dict-iteration order (hash-based), so we cannot + // assume the collateral vault is first. Iterate all vaults: collect collateral by type + // and convert any non-collateral vaults (FLOW overpayment dust) back to collateral. + 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 { + // destroy empty vault + Burner.burn(<-v) + } else { + // FLOW overpayment dust — convert back to collateral if routable + let quote = flowToCollateral.quoteOut(forProvided: v.balance, reverse: false) + if quote.outAmount > 0.0 { + let swapped <- flowToCollateral.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: flowToCollateral.getType().identifier + ) + Burner.burn(<-v) + } + } + } + + destroy resultVaults + + // Step 8: Drain any remaining syWFLOWv 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 sharesToCollateral = SwapConnectors.SequentialSwapper( + swappers: [syWFLOWvToFlow, flowToCollateral], + uniqueID: self.copyID() + ) + let quote = sharesToCollateral.quoteOut(forProvided: excessShares.balance, reverse: false) + if quote.outAmount > 0.0 { + let extraCollateral <- sharesToCollateral.swap(quote: quote, inVault: <-excessShares) + collateralVault.deposit(from: <-extraCollateral) + } else { + emit DustBurned( + tokenType: excessShares.getType().identifier, + balance: excessShares.balance, + quoteInType: quote.inType.identifier, + quoteOutType: quote.outType.identifier, + quoteInAmount: quote.inAmount, + quoteOutAmount: quote.outAmount, + swapperType: sharesToCollateral.getType().identifier + ) + Burner.burn(<-excessShares) + } + } else { + Burner.burn(<-excessShares) + } + + 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() { + FlowYieldVaultsAutoBalancersV1._cleanupAutoBalancer(id: self.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 + } + + /* =========================== + closePosition helpers + =========================== */ + + access(self) fun _getStoredMoreERC4626Config( + strategyType: Type, + collateralType: Type + ): MoreERC4626CollateralConfig? { + return FlowYieldVaultsStrategiesV2._getMoreERC4626Config( + composer: Type<@MoreERC4626StrategyComposer>(), + strategy: strategyType, + collateral: collateralType + ) + } + + /// Builds a syWFLOWv→FLOW UniV3 swapper from MoreERC4626CollateralConfig. + access(self) fun _buildSyWFLOWvToFlowSwapper( + closeConfig: FlowYieldVaultsStrategiesV2.MoreERC4626CollateralConfig, + closeTokens: FlowYieldVaultsStrategiesV2.TokenBundle, + uniqueID: DeFiActions.UniqueIdentifier + ): UniswapV3SwapConnectors.Swapper { + return FlowYieldVaultsStrategiesV2._buildUniV3Swapper( + tokenPath: closeConfig.yieldToUnderlyingUniV3AddressPath, + feePath: closeConfig.yieldToUnderlyingUniV3FeePath, + inVault: closeTokens.yieldTokenType, + outVault: closeTokens.underlying4626AssetType, // FlowToken.Vault + uniqueID: uniqueID + ) + } + + /// Builds a FLOW→collateral UniV3 swapper from MoreERC4626CollateralConfig. + access(self) fun _buildFlowToCollateralSwapper( + closeConfig: FlowYieldVaultsStrategiesV2.MoreERC4626CollateralConfig, + closeTokens: FlowYieldVaultsStrategiesV2.TokenBundle, + collateralType: Type, + uniqueID: DeFiActions.UniqueIdentifier + ): UniswapV3SwapConnectors.Swapper { + return FlowYieldVaultsStrategiesV2._buildUniV3Swapper( + tokenPath: closeConfig.debtToCollateralUniV3AddressPath, + feePath: closeConfig.debtToCollateralUniV3FeePath, + inVault: closeTokens.underlying4626AssetType, // FLOW + outVault: collateralType, + uniqueID: uniqueID + ) + } + } + access(all) struct TokenBundle { - /// The MOET token type (the pool's borrowable token) + /// @deprecated — retained for Cadence upgrade compatibility; populated with placeholder values and not read. access(all) let moetTokenType: Type access(all) let moetTokenEVMAddress: EVM.EVMAddress @@ -632,13 +1008,11 @@ access(all) contract FlowYieldVaultsStrategiesV2 { 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. + /// Resolves the token bundle for a strategy given the ERC4626 yield vault address. + /// moetTokenType/moetTokenEVMAddress are retained in TokenBundle for Cadence upgrade + /// compatibility (struct fields cannot be removed once deployed) but are no longer used — + /// the yield token address is passed as a placeholder to avoid unnecessary EVM lookups. 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())") @@ -648,8 +1022,8 @@ access(all) contract FlowYieldVaultsStrategiesV2 { ?? panic("Could not retrieve the VM Bridge associated Type for the ERC4626 underlying asset \(underlying4626AssetEVMAddress.toString())") return FlowYieldVaultsStrategiesV2.TokenBundle( - moetTokenType: moetTokenType, - moetTokenEVMAddress: moetTokenEVMAddress, + moetTokenType: yieldTokenType, // unused placeholder + moetTokenEVMAddress: yieldTokenEVMAddress, // unused placeholder yieldTokenType: yieldTokenType, yieldTokenEVMAddress: yieldTokenEVMAddress, underlying4626AssetType: underlying4626AssetType, @@ -808,10 +1182,16 @@ access(all) contract FlowYieldVaultsStrategiesV2 { switch type { // ----------------------------------------------------------------------- - // FUSDEVStrategy: borrows MOET from the FlowALP position, swaps to FUSDEV + // FUSDEVStrategy: borrows PYUSD0 from the FlowALP position, deposits into FUSDEV // ----------------------------------------------------------------------- case Type<@FUSDEVStrategy>(): - // Swappers: MOET <-> YIELD + // Reject PYUSD0 as collateral — it is the vault's underlying / debt token + assert( + collateralType != tokens.underlying4626AssetType, + message: "FUSDEVStrategy: PYUSD0 cannot be used as collateral — it is the vault's underlying asset" + ) + + // Swappers: PYUSD0 (underlying/debt) <-> YIELD (FUSDEV) let debtToYieldSwapper = self._createDebtToYieldSwapper(tokens: tokens, uniqueID: uniqueID) let yieldToDebtSwapper = self._createYieldToDebtSwapper(tokens: tokens, uniqueID: uniqueID) @@ -930,25 +1310,16 @@ access(all) contract FlowYieldVaultsStrategiesV2 { tokens: FlowYieldVaultsStrategiesV2.TokenBundle, uniqueID: DeFiActions.UniqueIdentifier ): SwapConnectors.MultiSwapper { - // Direct MOET -> YIELD via AMM + // Direct PYUSD0 → FUSDEV via AMM (fee 100) let debtToYieldAMM = self._createUniV3Swapper( - tokenPath: [tokens.moetTokenEVMAddress, tokens.yieldTokenEVMAddress], + tokenPath: [tokens.underlying4626AssetEVMAddress, tokens.yieldTokenEVMAddress], feePath: [100], - inVault: tokens.moetTokenType, + inVault: tokens.underlying4626AssetType, outVault: tokens.yieldTokenType, uniqueID: uniqueID ) - // MOET -> UNDERLYING via AMM - let debtToUnderlying = self._createUniV3Swapper( - tokenPath: [tokens.moetTokenEVMAddress, tokens.underlying4626AssetEVMAddress], - feePath: [100], - inVault: tokens.moetTokenType, - outVault: tokens.underlying4626AssetType, - uniqueID: uniqueID - ) - - // UNDERLYING -> YIELD via Morpho ERC4626 vault deposit + // PYUSD0 → FUSDEV via Morpho ERC4626 vault deposit (no AMM swap needed) let underlyingTo4626 = MorphoERC4626SwapConnectors.Swapper( vaultEVMAddress: tokens.yieldTokenEVMAddress, coa: FlowYieldVaultsStrategiesV2._getCOACapability(), @@ -957,15 +1328,10 @@ access(all) contract FlowYieldVaultsStrategiesV2 { isReversed: false ) - let seq = SwapConnectors.SequentialSwapper( - swappers: [debtToUnderlying, underlyingTo4626], - uniqueID: uniqueID - ) - return SwapConnectors.MultiSwapper( - inVault: tokens.moetTokenType, + inVault: tokens.underlying4626AssetType, outVault: tokens.yieldTokenType, - swappers: [debtToYieldAMM, seq], + swappers: [debtToYieldAMM, underlyingTo4626], uniqueID: uniqueID ) } @@ -974,16 +1340,16 @@ access(all) contract FlowYieldVaultsStrategiesV2 { tokens: FlowYieldVaultsStrategiesV2.TokenBundle, uniqueID: DeFiActions.UniqueIdentifier ): SwapConnectors.MultiSwapper { - // Direct YIELD -> MOET via AMM + // Direct FUSDEV → PYUSD0 via AMM (fee 100) let yieldToDebtAMM = self._createUniV3Swapper( - tokenPath: [tokens.yieldTokenEVMAddress, tokens.moetTokenEVMAddress], + tokenPath: [tokens.yieldTokenEVMAddress, tokens.underlying4626AssetEVMAddress], feePath: [100], inVault: tokens.yieldTokenType, - outVault: tokens.moetTokenType, + outVault: tokens.underlying4626AssetType, uniqueID: uniqueID ) - // YIELD -> UNDERLYING redeem via MorphoERC4626 vault + // FUSDEV → PYUSD0 via Morpho ERC4626 redeem (no additional AMM swap needed) let yieldToUnderlying = MorphoERC4626SwapConnectors.Swapper( vaultEVMAddress: tokens.yieldTokenEVMAddress, coa: FlowYieldVaultsStrategiesV2._getCOACapability(), @@ -991,24 +1357,11 @@ access(all) contract FlowYieldVaultsStrategiesV2 { 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 - ) - - let seq = SwapConnectors.SequentialSwapper( - swappers: [yieldToUnderlying, underlyingToDebt], - uniqueID: uniqueID - ) return SwapConnectors.MultiSwapper( inVault: tokens.yieldTokenType, - outVault: tokens.moetTokenType, - swappers: [yieldToDebtAMM, seq], + outVault: tokens.underlying4626AssetType, + swappers: [yieldToDebtAMM, yieldToUnderlying], uniqueID: uniqueID ) } @@ -1076,13 +1429,12 @@ access(all) contract FlowYieldVaultsStrategiesV2 { ) } - /// Creates a Collateral → Debt (MOET) swapper using UniswapV3. - /// Path: collateral → underlying (PYUSD0) → MOET + /// Creates a Collateral → Debt (PYUSD0) swapper using UniswapV3. + /// Path: collateral → underlying (PYUSD0) /// - /// 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. + /// The fee for collateral→underlying is the last fee in yieldToCollateral (reversed). + /// Used by FUSDEVStrategy.closePosition to pre-reduce position debt from + /// collateral when yield tokens alone cannot cover the full outstanding PYUSD0 debt. /// access(self) fun _createCollateralToDebtSwapper( collateralConfig: FlowYieldVaultsStrategiesV2.CollateralConfig, @@ -1094,51 +1446,194 @@ access(all) contract FlowYieldVaultsStrategiesV2 { 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 requires at least yield and collateral tokens, got \(yieldToCollPath.length)") + // underlying (PYUSD0) EVM address = second element of yieldToCollateral path (index 1) + 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) + // fee = collateral→PYUSD0 = last fee in yieldToCollateral (reversed) let collateralToUnderlyingFee = yieldToCollFees[yieldToCollFees.length - 1] - let underlyingToDebtFee: UInt32 = 100 return self._createUniV3Swapper( - tokenPath: [collateralEVMAddress, underlyingEVMAddress, tokens.moetTokenEVMAddress], - feePath: [collateralToUnderlyingFee, underlyingToDebtFee], + tokenPath: [collateralEVMAddress, underlyingEVMAddress], + feePath: [collateralToUnderlyingFee], inVault: collateralType, - outVault: tokens.moetTokenType, + outVault: tokens.underlying4626AssetType, uniqueID: uniqueID ) } } - access(all) entitlement Configure + /// 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}} - access(self) - fun makeCollateralConfig( - yieldTokenEVMAddress: EVM.EVMAddress, - yieldToCollateralAddressPath: [EVM.EVMAddress], - yieldToCollateralFeePath: [UInt32] - ): CollateralConfig { - pre { - yieldToCollateralAddressPath.length > 1: - "Invalid Uniswap V3 swap path length" - yieldToCollateralFeePath.length == yieldToCollateralAddressPath.length - 1: - "Uniswap V3 fee path length must be path length - 1" - yieldToCollateralAddressPath[0].equals(yieldTokenEVMAddress): - "UniswapV3 swap path must start with yield token" - } - - return CollateralConfig( - yieldTokenEVMAddress: yieldTokenEVMAddress, - yieldToCollateralUniV3AddressPath: yieldToCollateralAddressPath, - yieldToCollateralUniV3FeePath: yieldToCollateralFeePath - ) + 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). + 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 + ) + + // 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) + + return <-create syWFLOWvStrategy( + id: uniqueID, + collateralType: collateralType, + position: <-positionFlow + ) + + default: + panic("Unsupported strategy type \(type.identifier)") + } + } } - /// This resource enables the issuance of StrategyComposers, thus safeguarding the issuance of Strategies which + access(all) entitlement Configure + + +/// 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 { @@ -1160,12 +1655,15 @@ access(all) contract FlowYieldVaultsStrategiesV2 { 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 + Type<@MorphoERC4626StrategyComposer>(): true, + Type<@MoreERC4626StrategyComposer>(): true } } @@ -1186,6 +1684,7 @@ access(all) contract FlowYieldVaultsStrategiesV2 { access(self) view fun isSupportedComposer(_ type: Type): Bool { return type == Type<@MorphoERC4626StrategyComposer>() + || type == Type<@MoreERC4626StrategyComposer>() } access(all) fun issueComposer(_ type: Type): @{FlowYieldVaults.StrategyComposer} { @@ -1197,6 +1696,10 @@ access(all) contract FlowYieldVaultsStrategiesV2 { return <- create MorphoERC4626StrategyComposer( self.configs[type] ?? panic("No config registered for \(type.identifier)") ) + case Type<@MoreERC4626StrategyComposer>(): + let moreCfg = FlowYieldVaultsStrategiesV2._getMoreERC4626ComposerConfig(type) + assert(moreCfg.length > 0, message: "No config registered for \(type.identifier)") + return <- create MoreERC4626StrategyComposer(moreCfg) default: panic("Unsupported StrategyComposer \(type.identifier) requested") } @@ -1229,6 +1732,34 @@ 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, @@ -1243,20 +1774,47 @@ access(all) contract FlowYieldVaultsStrategiesV2 { "Collateral type \(collateralVaultType.identifier) is not a FungibleToken.Vault" } - let base = FlowYieldVaultsStrategiesV2.makeCollateralConfig( + let base = CollateralConfig( yieldTokenEVMAddress: yieldTokenEVMAddress, - yieldToCollateralAddressPath: yieldToCollateralAddressPath, - yieldToCollateralFeePath: yieldToCollateralFeePath + yieldToCollateralUniV3AddressPath: yieldToCollateralAddressPath, + yieldToCollateralUniV3FeePath: yieldToCollateralFeePath ) 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 = MoreERC4626CollateralConfig( + yieldTokenEVMAddress: yieldTokenEVMAddress, + yieldToUnderlyingUniV3AddressPath: yieldToUnderlyingAddressPath, + yieldToUnderlyingUniV3FeePath: yieldToUnderlyingFeePath, + debtToCollateralUniV3AddressPath: debtToCollateralAddressPath, + debtToCollateralUniV3FeePath: debtToCollateralFeePath + ) + self.upsertMoreERC4626Config(config: { strategyType: { collateralVaultType: cfg } }) + } + access(Configure) fun purgeConfig() { self.configs = { Type<@MorphoERC4626StrategyComposer>(): { Type<@FUSDEVStrategy>(): {} as {Type: CollateralConfig} } } + FlowYieldVaultsStrategiesV2._purgeMoreERC4626Configs() } } @@ -1348,6 +1906,53 @@ access(all) contract FlowYieldVaultsStrategiesV2 { ) } + // --- "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}}} + } + init( univ3FactoryEVMAddress: String, univ3RouterEVMAddress: String, @@ -1359,11 +1964,6 @@ access(all) contract FlowYieldVaultsStrategiesV2 { self.IssuerStoragePath = StoragePath(identifier: "FlowYieldVaultsStrategyV2ComposerIssuer_\(self.account.address)")! self.config = {} - let moetType = Type<@MOET.Vault>() - if FlowEVMBridgeConfig.getEVMAddressAssociated(with: Type<@MOET.Vault>()) == nil { - panic("Could not find EVM address for \(moetType.identifier) - ensure the asset is onboarded to the VM Bridge") - } - let issuer <- create StrategyComposerIssuer( configs: { Type<@MorphoERC4626StrategyComposer>(): { diff --git a/cadence/scripts/tokens/get_vault_balance_by_type.cdc b/cadence/scripts/tokens/get_vault_balance_by_type.cdc new file mode 100644 index 00000000..8acd64f3 --- /dev/null +++ b/cadence/scripts/tokens/get_vault_balance_by_type.cdc @@ -0,0 +1,33 @@ +import "FungibleToken" +import "FungibleTokenMetadataViews" +import "ViewResolver" + +/// Returns the Cadence FungibleToken balance for the given account, resolving the +/// vault's public metadata path dynamically via the FTVaultData metadata view. +/// +/// Useful for checking balances of VM-bridged ERC-20 tokens without hard-coding public paths. +/// +/// @param address: The account address to check +/// @param vaultIdentifier: The Cadence type identifier (e.g. "A.1e4aa0b87d10b141.EVMVMBridgedToken_...Vault") +/// @return UFix64?: The vault balance, or nil if the vault is not set up for this account +/// +access(all) fun main(address: Address, vaultIdentifier: String): UFix64? { + let vaultType = CompositeType(vaultIdentifier) + if vaultType == nil { return nil } + + let contractAddr = vaultType!.address + if contractAddr == nil { return nil } + let contractName = vaultType!.contractName + if contractName == nil { return nil } + + let viewResolver = getAccount(contractAddr!).contracts.borrow<&{ViewResolver}>(name: contractName!) + if viewResolver == nil { return nil } + + let vaultData = viewResolver!.resolveContractView( + resourceType: vaultType!, + viewType: Type() + ) as? FungibleTokenMetadataViews.FTVaultData + if vaultData == nil { return nil } + + return getAccount(address).capabilities.borrow<&{FungibleToken.Vault}>(vaultData!.metadataPath)?.balance +} diff --git a/cadence/tests/FlowYieldVaultsStrategiesV2_FUSDEV_test.cdc b/cadence/tests/FlowYieldVaultsStrategiesV2_FUSDEV_test.cdc index df0b7d68..2fce0197 100644 --- a/cadence/tests/FlowYieldVaultsStrategiesV2_FUSDEV_test.cdc +++ b/cadence/tests/FlowYieldVaultsStrategiesV2_FUSDEV_test.cdc @@ -17,8 +17,8 @@ import "FlowYieldVaultsClosedBeta" /// 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 +/// → FlowALP borrow PYUSD0 → ERC4626 deposit → FUSDEV (Morpho vault) +/// Close: FUSDEV → PYUSD0 (ERC4626 redeem) → repay FlowALP → returned to user /// /// Mainnet addresses: /// - Admin (FlowYieldVaults deployer): 0xb1d63873c3cc9f79 @@ -42,6 +42,10 @@ access(all) let adminAccount = Test.getAccount(0xb1d63873c3cc9f79) /// Used for WFLOW lifecycle tests and for the negative PYUSD0 collateral test. access(all) let flowUser = Test.getAccount(0x443472749ebdaac8) +/// Large PYUSD0 holder (~70k PYUSD0) — used solely to seed the FlowALP pool's +/// PYUSD0 reserves so the pool can service PYUSD0 drawdowns for FUSDEVStrategy positions. +access(all) let pyusd0Holder = Test.getAccount(0x24263c125b7770e0) + /// FlowToken contract account — used to provision FLOW to flowUser in setup. access(all) let flowTokenAccount = Test.getAccount(0x1654653399040a61) @@ -81,6 +85,9 @@ access(all) var flowVaultID: UInt64 = 0 access(all) var wbtcVaultID: UInt64 = 0 access(all) var wethVaultID: UInt64 = 0 +/// Relative tolerance used in all balance assertions (1%). +access(all) let tolerancePct: UFix64 = 0.01 + /* --- Helpers --- */ access(all) @@ -115,6 +122,31 @@ fun _latestVaultID(_ user: Test.TestAccount): UInt64 { return ids![ids!.length - 1] } +/// Returns the FUSDEV share balance held in the AutoBalancer for the given vault ID, +/// or nil if no AutoBalancer exists. +access(all) +fun _autoBalancerBalance(_ vaultID: UInt64): UFix64? { + let r = _executeScript("../scripts/flow-yield-vaults/get_auto_balancer_balance_by_id.cdc", [vaultID]) + Test.expect(r, Test.beSucceeded()) + return r.returnValue as? UFix64 +} + +/// Returns the WETH Cadence vault balance for the given account. +access(all) +fun _wethBalance(_ user: Test.TestAccount): UFix64 { + let r = _executeScript("../scripts/tokens/get_vault_balance_by_type.cdc", [user.address, wethVaultIdentifier]) + Test.expect(r, Test.beSucceeded()) + return (r.returnValue as? UFix64) ?? 0.0 +} + +/// Returns the native FLOW balance for the given account. +access(all) +fun _flowBalance(_ user: Test.TestAccount): UFix64 { + let r = _executeScript("../scripts/flow-yield-vaults/get_flow_balance.cdc", [user.address]) + Test.expect(r, Test.beSucceeded()) + return (r.returnValue as? UFix64) ?? 0.0 +} + /* --- Setup --- */ access(all) fun setup() { @@ -193,7 +225,15 @@ access(all) fun setup() { ) Test.expect(err, Test.beNil()) - log("Deploying FlowYieldVaultsAutoBalancers...") + log("Deploying UInt64LinkedList...") + err = Test.deployContract( + name: "UInt64LinkedList", + path: "../../cadence/contracts/UInt64LinkedList.cdc", + arguments: [] + ) + Test.expect(err, Test.beNil()) + + log("Deploying AutoBalancers...") err = Test.deployContract( name: "AutoBalancers", path: "../../cadence/contracts/AutoBalancers.cdc", @@ -201,6 +241,14 @@ access(all) fun setup() { ) Test.expect(err, Test.beNil()) + log("Deploying FlowYieldVaultsSchedulerRegistryV1...") + err = Test.deployContract( + name: "FlowYieldVaultsSchedulerRegistryV1", + path: "../../cadence/contracts/FlowYieldVaultsSchedulerRegistryV1.cdc", + arguments: [] + ) + Test.expect(err, Test.beNil()) + log("Deploying FlowYieldVaultsAutoBalancers...") err = Test.deployContract( name: "FlowYieldVaultsAutoBalancers", @@ -209,18 +257,25 @@ access(all) fun setup() { ) 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()) + log("Deploying FlowYieldVaultsAutoBalancersV1...") + err = Test.deployContract( + name: "FlowYieldVaultsAutoBalancersV1", + path: "../../cadence/contracts/FlowYieldVaultsAutoBalancersV1.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). @@ -295,6 +350,28 @@ access(all) fun setup() { ) Test.expect(result, Test.beSucceeded()) + // Seed the FlowALP pool with PYUSD0 reserves. + // FUSDEVStrategy borrows PYUSD0 as its debt token (drawDownSink expects PYUSD0). + // The pool can mint MOET but must draw non-MOET tokens from reserves[tokenType]. + // pyusd0Holder (0x24263c125b7770e0) holds ~70k PYUSD0 on mainnet — grant pool + // access and have them deposit 1000 PYUSD0 so the pool can service drawdowns. + let alpAdmin = Test.getAccount(0x6b00ff876c299c61) + log("Granting pyusd0Holder FlowALP pool cap for PYUSD0 reserve position...") + result = _executeTransactionFile( + "../../lib/FlowALP/cadence/tests/transactions/flow-alp/pool-management/03_grant_beta.cdc", + [], + [alpAdmin, pyusd0Holder] + ) + Test.expect(result, Test.beSucceeded()) + + log("Creating 1000 PYUSD0 reserve position in FlowALP pool (pushToDrawDownSink: false)...") + result = _executeTransactionFile( + "../../lib/FlowALP/cadence/transactions/flow-alp/position/create_position.cdc", + [1000.0 as UFix64, /storage/EVMVMBridgedToken_99af3eea856556646c98c8b9b2548fe815240750Vault as StoragePath, false as Bool], + [pyusd0Holder] + ) + 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...") @@ -368,7 +445,7 @@ access(all) fun testDepositToFUSDEVYieldVault_WFLOW() { 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), + Test.assert(equalAmounts(a: after, b: before + depositAmount, tolerance: (before + depositAmount) * tolerancePct), message: "WFLOW deposit: expected ~".concat((before + depositAmount).toString()).concat(", got ").concat(after.toString())) log("WFLOW vault balance after deposit: ".concat(after.toString())) } @@ -382,7 +459,7 @@ access(all) fun testWithdrawFromFUSDEVYieldVault_WFLOW() { 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), + Test.assert(equalAmounts(a: after, b: before - withdrawAmount, tolerance: (before - withdrawAmount) * tolerancePct), message: "WFLOW withdraw: expected ~".concat((before - withdrawAmount).toString()).concat(", got ").concat(after.toString())) log("WFLOW vault balance after withdrawal: ".concat(after.toString())) } @@ -402,7 +479,7 @@ access(all) fun testCloseFUSDEVYieldVault_WFLOW() { // 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), + Test.assert(equalAmounts(a: collateralAfter, b: collateralBefore + vaultBalBefore, tolerance: vaultBalBefore * tolerancePct), 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())) } @@ -439,7 +516,7 @@ access(all) fun testDepositToFUSDEVYieldVault_WBTC() { 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), + Test.assert(equalAmounts(a: after, b: before + depositAmount, tolerance: (before + depositAmount) * tolerancePct), message: "WBTC deposit: expected ~".concat((before + depositAmount).toString()).concat(", got ").concat(after.toString())) log("WBTC vault balance after deposit: ".concat(after.toString())) } @@ -453,7 +530,7 @@ access(all) fun testWithdrawFromFUSDEVYieldVault_WBTC() { 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), + Test.assert(equalAmounts(a: after, b: before - withdrawAmount, tolerance: (before - withdrawAmount) * tolerancePct), message: "WBTC withdraw: expected ~".concat((before - withdrawAmount).toString()).concat(", got ").concat(after.toString())) log("WBTC vault balance after withdrawal: ".concat(after.toString())) } @@ -473,7 +550,7 @@ access(all) fun testCloseFUSDEVYieldVault_WBTC() { 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), + Test.assert(equalAmounts(a: collateralAfter, b: collateralBefore + vaultBalBefore, tolerance: vaultBalBefore * tolerancePct), 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())) } @@ -510,7 +587,7 @@ access(all) fun testDepositToFUSDEVYieldVault_WETH() { 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), + Test.assert(equalAmounts(a: after, b: before + depositAmount, tolerance: (before + depositAmount) * tolerancePct), message: "WETH deposit: expected ~".concat((before + depositAmount).toString()).concat(", got ").concat(after.toString())) log("WETH vault balance after deposit: ".concat(after.toString())) } @@ -524,7 +601,7 @@ access(all) fun testWithdrawFromFUSDEVYieldVault_WETH() { 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), + Test.assert(equalAmounts(a: after, b: before - withdrawAmount, tolerance: (before - withdrawAmount) * tolerancePct), message: "WETH withdraw: expected ~".concat((before - withdrawAmount).toString()).concat(", got ").concat(after.toString())) log("WETH vault balance after withdrawal: ".concat(after.toString())) } @@ -544,7 +621,7 @@ access(all) fun testCloseFUSDEVYieldVault_WETH() { 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), + Test.assert(equalAmounts(a: collateralAfter, b: collateralBefore + vaultBalBefore, tolerance: vaultBalBefore * tolerancePct), 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())) } @@ -590,3 +667,114 @@ access(all) fun testCannotDepositWrongTokenToYieldVault() { Test.expect(depositResult, Test.beFailed()) log("Correctly rejected wrong-token deposit (WBTC into WETH vault)") } + +/* ========================================================= + Excess-yield tests + ========================================================= */ + +/// Opens a FUSDEVStrategy WFLOW vault, injects extra FUSDEV to create an excess scenario, +/// closes the vault, and verifies that the excess is returned as FLOW to the user. +/// +/// Using FLOW as collateral makes the excess-return clearly visible: the user ends up with +/// more FLOW than they started with, because the injected FUSDEV shares are converted back +/// to FLOW (via FUSDEV → PYUSD0 → WFLOW) and added to the returned collateral. +/// +/// Scenario: +/// 1. Open a FUSDEVStrategy vault with 10.0 FLOW. +/// 2. Convert 5.0 PYUSD0 → FUSDEV and deposit directly into the AutoBalancer. +/// (flowUser already holds PYUSD0 on mainnet — no transfer needed.) +/// → AutoBalancer balance now exceeds what is needed to repay the PYUSD0 debt. +/// 3. Close the vault. +/// → Step 9 of closePosition() drains the remaining FUSDEV, converts it to +/// FLOW via MultiSwapper (FUSDEV → PYUSD0 → WFLOW), and adds it to the +/// returned collateral. +/// 4. Verify flowAfter > flowBefore: the user gained net FLOW from the excess. +access(all) fun testCloseFUSDEVVaultWithExcessYieldTokens_WFLOW() { + log("=== testCloseFUSDEVVaultWithExcessYieldTokens_WFLOW ===") + + let flowBefore = _flowBalance(flowUser) + log("FLOW balance before vault creation: \(flowBefore)") + + let collateralAmount: UFix64 = 10.0 + log("Creating FUSDEVStrategy vault with \(collateralAmount) FLOW...") + let createResult = _executeTransactionFile( + "../transactions/flow-yield-vaults/create_yield_vault.cdc", + [fusdEvStrategyIdentifier, flowVaultIdentifier, collateralAmount], + [flowUser] + ) + Test.expect(createResult, Test.beSucceeded()) + + let vaultID = _latestVaultID(flowUser) + log("Created vault ID: \(vaultID)") + + let vaultBalAfterCreate = _executeScript( + "../scripts/flow-yield-vaults/get_yield_vault_balance.cdc", + [flowUser.address, vaultID] + ) + Test.expect(vaultBalAfterCreate, Test.beSucceeded()) + let vaultBal = vaultBalAfterCreate.returnValue! as! UFix64? + Test.assert(equalAmounts(a: vaultBal!, b: collateralAmount, tolerance: collateralAmount * tolerancePct), + message: "Expected vault balance ~\(collateralAmount) after create, got: \(vaultBal ?? 0.0)") + log("Vault balance (FLOW collateral value): \(vaultBal!)") + + let abBalBefore = _autoBalancerBalance(vaultID) + Test.assert(abBalBefore! > 0.0, + message: "Expected positive AutoBalancer balance after vault creation, got: \(abBalBefore ?? 0.0)") + log("AutoBalancer FUSDEV balance before injection: \(abBalBefore!)") + + // flowUser already holds PYUSD0 on mainnet — inject directly without a transfer. + let injectionPYUSD0Amount: UFix64 = 5.0 + log("Injecting \(injectionPYUSD0Amount) PYUSD0 worth of FUSDEV into AutoBalancer...") + let injectResult = _executeTransactionFile( + "transactions/inject_pyusd0_as_fusdev_to_autobalancer.cdc", + [vaultID, fusdEvEVMAddress, injectionPYUSD0Amount], + [flowUser] + ) + Test.expect(injectResult, Test.beSucceeded()) + + let abBalAfter = _autoBalancerBalance(vaultID) + Test.assert(abBalAfter != nil, + message: "AutoBalancer should still exist after injection") + Test.assert(abBalAfter! > abBalBefore!, + message: "AutoBalancer FUSDEV balance should have increased after injection. Before: \(abBalBefore!) After: \(abBalAfter!)") + let injectedShares = abBalAfter! - abBalBefore! + log("AutoBalancer FUSDEV balance after injection: \(abBalAfter!)") + log("Injected \(injectedShares) FUSDEV shares (excess over original debt coverage)") + + log("Closing vault \(vaultID)...") + let closeResult = _executeTransactionFile( + "../transactions/flow-yield-vaults/close_yield_vault.cdc", + [vaultID], + [flowUser] + ) + Test.expect(closeResult, Test.beSucceeded()) + + let vaultBalAfterClose = _executeScript( + "../scripts/flow-yield-vaults/get_yield_vault_balance.cdc", + [flowUser.address, vaultID] + ) + Test.expect(vaultBalAfterClose, Test.beSucceeded()) + Test.assert(vaultBalAfterClose.returnValue == nil, + message: "Vault \(vaultID) should not exist after close") + log("Vault no longer exists — close confirmed") + + let abBalFinal = _autoBalancerBalance(vaultID) + Test.assert(abBalFinal == nil, + message: "AutoBalancer should be nil (burned) after vault close, but got: \(abBalFinal ?? 0.0)") + log("AutoBalancer is nil after close — torn down during _cleanupAutoBalancer") + + let flowAfter = _flowBalance(flowUser) + log("FLOW balance after close: \(flowAfter)") + + // 5 PYUSD0 ≈ $5 at current prices — well above tx fees incurred during this test. + // The net gain should be clearly positive: excess FUSDEV → PYUSD0 → WFLOW adds more + // FLOW back than the transactions consume in fees. + Test.assert( + flowAfter > flowBefore, + message: "User should have more FLOW than before (excess FUSDEV converted back to FLOW). Before: \(flowBefore), After: \(flowAfter)" + ) + let flowNet = flowAfter - flowBefore + log("Net FLOW gain from excess FUSDEV conversion: \(flowNet) FLOW (injected ~\(injectionPYUSD0Amount) PYUSD0 worth)") + + log("=== testCloseFUSDEVVaultWithExcessYieldTokens_WFLOW PASSED ===") +} diff --git a/cadence/tests/FlowYieldVaultsStrategiesV2_syWFLOWv_test.cdc b/cadence/tests/FlowYieldVaultsStrategiesV2_syWFLOWv_test.cdc new file mode 100644 index 00000000..19c19ba7 --- /dev/null +++ b/cadence/tests/FlowYieldVaultsStrategiesV2_syWFLOWv_test.cdc @@ -0,0 +1,788 @@ +#test_fork(network: "mainnet", height: nil) + +import Test + +import "EVM" +import "FlowToken" +import "FlowALPv0" +import "FlowYieldVaults" +import "FlowYieldVaultsClosedBeta" + +/// Fork test for FlowYieldVaultsStrategiesV2 syWFLOWvStrategy. +/// +/// Tests the full YieldVault lifecycle (create, deposit, withdraw, close) for each supported +/// collateral type: PYUSD0, WBTC, and WETH. +/// +/// FLOW cannot be used as collateral — it is the vault's underlying / debt asset. +/// +/// Strategy: +/// → FlowALP borrow FLOW → ERC4626 deposit → syWFLOWv (More vault) +/// Close: syWFLOWv → FLOW via UniV3 (repay) → returned to user +/// +/// Mainnet addresses: +/// - Admin (FlowYieldVaults deployer): 0xb1d63873c3cc9f79 +/// - PYUSD0 user: 0x443472749ebdaac8 (pre-holds PYUSD0 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 +/// - WFLOW: 0xd3bF53DAC106A0290B0483EcBC89d40FcC961f3e +/// - syWFLOWv (More vault): 0xCBf9a7753F9D2d0e8141ebB36d99f87AcEf98597 +/// - PYUSD0: 0x99aF3EeA856556646C98c8B9b2548Fe815240750 +/// - WBTC: 0x717DAE2BaF7656BE9a9B01deE31d571a9d4c9579 (cbBTC, no WFLOW pool; use WETH as intermediate) +/// - WETH: 0x2F6F07CDcf3588944Bf4C42aC74ff24bF56e7590 (WFLOW/WETH pool fee 3000 exists) + +// --- Accounts --- + +/// Mainnet admin account — deployer of FlowYieldVaults, FlowYieldVaultsClosedBeta, FlowYieldVaultsStrategiesV2 +access(all) let adminAccount = Test.getAccount(0xb1d63873c3cc9f79) + +/// PYUSD0 holder on mainnet +access(all) let pyusd0User = Test.getAccount(0x443472749ebdaac8) + +/// 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 syWFLOWvStrategyIdentifier = "A.b1d63873c3cc9f79.FlowYieldVaultsStrategiesV2.syWFLOWvStrategy" +access(all) let composerIdentifier = "A.b1d63873c3cc9f79.FlowYieldVaultsStrategiesV2.MoreERC4626StrategyComposer" +access(all) let issuerStoragePath: StoragePath = /storage/FlowYieldVaultsStrategyV2ComposerIssuer_0xb1d63873c3cc9f79 + +// --- Cadence Vault Type Identifiers (VM-bridged ERC-20s) --- + +access(all) let pyusd0VaultIdentifier = "A.1e4aa0b87d10b141.EVMVMBridgedToken_99af3eea856556646c98c8b9b2548fe815240750.Vault" +access(all) let wbtcVaultIdentifier = "A.1e4aa0b87d10b141.EVMVMBridgedToken_717dae2baf7656be9a9b01dee31d571a9d4c9579.Vault" +access(all) let wethVaultIdentifier = "A.1e4aa0b87d10b141.EVMVMBridgedToken_2f6f07cdcf3588944bf4c42ac74ff24bf56e7590.Vault" + +// --- EVM Addresses --- + +access(all) let syWFLOWvEVMAddress = "0xCBf9a7753F9D2d0e8141ebB36d99f87AcEf98597" +access(all) let wflowEVMAddress = "0xd3bF53DAC106A0290B0483EcBC89d40FcC961f3e" +access(all) let pyusd0EVMAddress = "0x99aF3EeA856556646C98c8B9b2548Fe815240750" +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 pyusd0VaultID: UInt64 = 0 +access(all) var wbtcVaultID: UInt64 = 0 +access(all) var wethVaultID: UInt64 = 0 + +/// Relative tolerance used in all balance assertions (0.1%). +access(all) let tolerancePct: UFix64 = 0.001 + + +/* --- 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 latest yield vault ID for the given account, or panics if none found. +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 \(user.address)") + return ids![ids!.length - 1] +} + +/// Returns the syWFLOWv share balance held in the AutoBalancer for the given vault ID, +/// or nil if no AutoBalancer exists. +access(all) +fun _autoBalancerBalance(_ vaultID: UInt64): UFix64? { + let r = _executeScript("../scripts/flow-yield-vaults/get_auto_balancer_balance_by_id.cdc", [vaultID]) + Test.expect(r, Test.beSucceeded()) + return r.returnValue as? UFix64 +} + +/// Returns the WETH Cadence vault balance for the given account. +access(all) +fun _wethBalance(_ user: Test.TestAccount): UFix64 { + let r = _executeScript("../scripts/tokens/get_vault_balance_by_type.cdc", [user.address, wethVaultIdentifier]) + Test.expect(r, Test.beSucceeded()) + return (r.returnValue as? UFix64) ?? 0.0 +} + +/// Returns the PYUSD0 Cadence vault balance for the given account. +access(all) +fun _pyusd0Balance(_ user: Test.TestAccount): UFix64 { + let r = _executeScript("../scripts/tokens/get_vault_balance_by_type.cdc", [user.address, pyusd0VaultIdentifier]) + Test.expect(r, Test.beSucceeded()) + return (r.returnValue as? UFix64) ?? 0.0 +} + +/// Returns the WBTC Cadence vault balance for the given account. +access(all) +fun _wbtcBalance(_ user: Test.TestAccount): UFix64 { + let r = _executeScript("../scripts/tokens/get_vault_balance_by_type.cdc", [user.address, wbtcVaultIdentifier]) + Test.expect(r, Test.beSucceeded()) + return (r.returnValue as? UFix64) ?? 0.0 +} + +/* --- Setup --- */ + +access(all) fun setup() { + log("==== FlowYieldVaultsStrategiesV2 syWFLOWv 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()) + + log("Deploying ERC4626PriceOracles...") + err = Test.deployContract( + name: "ERC4626PriceOracles", + path: "../../lib/FlowALP/FlowActions/cadence/contracts/connectors/evm/ERC4626PriceOracles.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 FlowALPv0...") + err = Test.deployContract( + name: "FlowALPv0", + path: "../../lib/FlowALP/cadence/contracts/FlowALPv0.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 UInt64LinkedList...") + err = Test.deployContract( + name: "UInt64LinkedList", + path: "../../cadence/contracts/UInt64LinkedList.cdc", + arguments: [] + ) + Test.expect(err, Test.beNil()) + + log("Deploying AutoBalancers...") + err = Test.deployContract( + name: "AutoBalancers", + path: "../../cadence/contracts/AutoBalancers.cdc", + arguments: [] + ) + Test.expect(err, Test.beNil()) + + log("Deploying FlowYieldVaultsSchedulerRegistryV1...") + err = Test.deployContract( + name: "FlowYieldVaultsSchedulerRegistryV1", + path: "../../cadence/contracts/FlowYieldVaultsSchedulerRegistryV1.cdc", + arguments: [] + ) + Test.expect(err, Test.beNil()) + + log("Deploying FlowYieldVaultsAutoBalancersV1...") + err = Test.deployContract( + name: "FlowYieldVaultsAutoBalancersV1", + path: "../../cadence/contracts/FlowYieldVaultsAutoBalancersV1.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()) + + // yieldToUnderlying path is the same for all collaterals: syWFLOWv → WFLOW via UniV3 fee 100 (0.01%) + // debtToCollateral paths differ per collateral: WFLOW → + + log("Configuring MoreERC4626CollateralConfig: syWFLOWvStrategy + PYUSD0 (WFLOW→PYUSD0 fee 3000)...") + var result = _executeTransactionFile( + "../transactions/flow-yield-vaults/admin/upsert_more_erc4626_config.cdc", + [ + syWFLOWvStrategyIdentifier, + pyusd0VaultIdentifier, + syWFLOWvEVMAddress, + [syWFLOWvEVMAddress, wflowEVMAddress], // yieldToUnderlying + [100 as UInt32], + [wflowEVMAddress, pyusd0EVMAddress], // debtToCollateral + [3000 as UInt32] + ], + [adminAccount] + ) + Test.expect(result, Test.beSucceeded()) + + // No WFLOW/WBTC pool exists on Flow EVM; use 2-hop path WFLOW→WETH→WBTC instead. + log("Configuring MoreERC4626CollateralConfig: syWFLOWvStrategy + WBTC (WFLOW→WETH→WBTC fee 3000/3000)...") + result = _executeTransactionFile( + "../transactions/flow-yield-vaults/admin/upsert_more_erc4626_config.cdc", + [ + syWFLOWvStrategyIdentifier, + wbtcVaultIdentifier, + syWFLOWvEVMAddress, + [syWFLOWvEVMAddress, wflowEVMAddress], // yieldToUnderlying: syWFLOWv→WFLOW + [100 as UInt32], + [wflowEVMAddress, wethEVMAddress, wbtcEVMAddress], // debtToCollateral: WFLOW→WETH→WBTC + [3000 as UInt32, 3000 as UInt32] + ], + [adminAccount] + ) + Test.expect(result, Test.beSucceeded()) + + log("Configuring MoreERC4626CollateralConfig: syWFLOWvStrategy + WETH (WFLOW→WETH fee 3000)...") + result = _executeTransactionFile( + "../transactions/flow-yield-vaults/admin/upsert_more_erc4626_config.cdc", + [ + syWFLOWvStrategyIdentifier, + wethVaultIdentifier, + syWFLOWvEVMAddress, + [syWFLOWvEVMAddress, wflowEVMAddress], // yieldToUnderlying + [100 as UInt32], + [wflowEVMAddress, wethEVMAddress], // debtToCollateral + [3000 as UInt32] + ], + [adminAccount] + ) + Test.expect(result, Test.beSucceeded()) + + // Register syWFLOWvStrategy in the FlowYieldVaults StrategyFactory + log("Registering syWFLOWvStrategy in FlowYieldVaults factory...") + result = _executeTransactionFile( + "../transactions/flow-yield-vaults/admin/add_strategy_composer.cdc", + [syWFLOWvStrategyIdentifier, composerIdentifier, issuerStoragePath], + [adminAccount] + ) + Test.expect(result, Test.beSucceeded()) + + // Grant beta access to all user accounts + log("Granting beta access to PYUSD0 user...") + result = _executeTransactionFile( + "../transactions/flow-yield-vaults/admin/grant_beta.cdc", + [], + [adminAccount, pyusd0User] + ) + 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()) + + // Add FLOW reserves to the FlowALP pool. + // The mainnet pool at 0x6b00ff876c299c61 only has ~12 FLOW at the fork height — + // not enough for WBTC/WETH vaults (WBTC ~$9 needs ~125 FLOW; WETH ~$2.5 needs ~35 FLOW). + // wbtcUser holds 1.38M FLOW in Cadence storage, so we grant them pool access and + // have them create a 10,000-FLOW reserve position. + let alpAdmin = Test.getAccount(0x6b00ff876c299c61) + log("Granting wbtcUser FlowALP pool cap for reserve position...") + result = _executeTransactionFile( + "../../lib/FlowALP/cadence/tests/transactions/flow-alp/pool-management/03_grant_beta.cdc", + [], + [alpAdmin, wbtcUser] + ) + Test.expect(result, Test.beSucceeded()) + + log("Creating 10,000 FLOW reserve position in FlowALP pool...") + result = _executeTransactionFile( + "../../lib/FlowALP/cadence/transactions/flow-alp/position/create_position.cdc", + [10000.0 as UFix64, /storage/flowTokenVault, true as Bool], + [wbtcUser] + ) + Test.expect(result, Test.beSucceeded()) + + // Provision WETH: bridge ~2 WETH from the COA (EVM) to Cadence storage. + // The COA at 0x000000000000000000000002b87c966bc00bc2c4 holds ~12 WETH on mainnet. + log("Bridging 2 WETH from COA to Cadence for WBTC/WETH user...") + // 2 WETH = 2_000_000_000_000_000_000 (18 decimals) + result = _executeTransactionFile( + "../../lib/FlowALP/FlowActions/cadence/tests/transactions/bridge/bridge_tokens_from_evm.cdc", + [wethVaultIdentifier, 2000000000000000000 as UInt256], + [wethUser] + ) + Test.expect(result, Test.beSucceeded()) + + // Provision WBTC: swap 0.1 WETH → WBTC via the UniV3 WETH/WBTC pool (fee 3000). + log("Swapping 0.1 WETH → WBTC for WBTC test user...") + result = _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(result, Test.beSucceeded()) + + log("==== Setup Complete ====") +} + +/* ========================================================= + PYUSD0 collateral lifecycle + ========================================================= */ + +access(all) fun testCreateSyWFLOWvYieldVault_PYUSD0() { + let collateralAmount: UFix64 = 2.0 + log("Creating syWFLOWvStrategy yield vault with \(collateralAmount) PYUSD0...") + let result = _executeTransactionFile( + "../transactions/flow-yield-vaults/create_yield_vault.cdc", + [syWFLOWvStrategyIdentifier, pyusd0VaultIdentifier, collateralAmount], + [pyusd0User] + ) + Test.expect(result, Test.beSucceeded()) + + pyusd0VaultID = _latestVaultID(pyusd0User) + log("Created PYUSD0 vault ID: \(pyusd0VaultID)") + + let bal = _executeScript("../scripts/flow-yield-vaults/get_yield_vault_balance.cdc", [pyusd0User.address, pyusd0VaultID]) + Test.expect(bal, Test.beSucceeded()) + let balance = bal.returnValue! as! UFix64? + Test.assert(equalAmounts(a: balance!, b: collateralAmount, tolerance: collateralAmount * tolerancePct), + message: "PYUSD0 vault balance after create should be ~\(collateralAmount), got \(balance!)") + log("PYUSD0 vault balance after create: \(balance!)") + + // Verify PYUSD0 was deposited directly into FlowALP as collateral (no intermediate token swap). + // syWFLOWvStrategy does not involve MOET at any point — PYUSD0 is deposited as-is. + // - There must be a Deposited event with vaultType = PYUSD0 (collateral deposited directly) + // - There must be NO Deposited event with vaultType = MOET (no pre-swap should occur) + let depositedEvents = Test.eventsOfType(Type()) + log("FlowALPv0.Deposited events: \(depositedEvents.length)") + + let moetTypeID = "A.6b00ff876c299c61.MOET.Vault" + var foundMoetDeposit = false + var foundPyusd0Deposit = false + for e in depositedEvents { + let ev = e as! FlowALPv0.Deposited + log(" Deposited: vaultType=\(ev.vaultType.identifier) amount=\(ev.amount)") + if ev.vaultType.identifier == moetTypeID { + foundMoetDeposit = true + } + if ev.vaultType.identifier == pyusd0VaultIdentifier { + foundPyusd0Deposit = true + } + } + Test.assert(foundPyusd0Deposit, + message: "Expected FlowALPv0.Deposited event with PYUSD0 — PYUSD0 collateral was not deposited into FlowALP") + Test.assert(!foundMoetDeposit, + message: "Unexpected FlowALPv0.Deposited event with MOET — syWFLOWvStrategy should not involve MOET") + log("Confirmed: FlowALP received PYUSD0 directly as collateral (no MOET involvement)") +} + +access(all) fun testDepositToSyWFLOWvYieldVault_PYUSD0() { + let before = (_executeScript("../scripts/flow-yield-vaults/get_yield_vault_balance.cdc", [pyusd0User.address, pyusd0VaultID]).returnValue! as! UFix64?) ?? 0.0 + let depositAmount: UFix64 = 0.5 + log("Depositing 0.5 PYUSD0 to vault \(pyusd0VaultID)...") + Test.expect( + _executeTransactionFile("../transactions/flow-yield-vaults/deposit_to_yield_vault.cdc", [pyusd0VaultID, depositAmount], [pyusd0User]), + Test.beSucceeded() + ) + let after = (_executeScript("../scripts/flow-yield-vaults/get_yield_vault_balance.cdc", [pyusd0User.address, pyusd0VaultID]).returnValue! as! UFix64?)! + Test.assert(equalAmounts(a: after, b: before + depositAmount, tolerance: (before + depositAmount) * tolerancePct), + message: "PYUSD0 deposit: expected ~\(before + depositAmount), got \(after)") + log("PYUSD0 vault balance after deposit: \(after)") +} + +access(all) fun testWithdrawFromSyWFLOWvYieldVault_PYUSD0() { + let before = (_executeScript("../scripts/flow-yield-vaults/get_yield_vault_balance.cdc", [pyusd0User.address, pyusd0VaultID]).returnValue! as! UFix64?) ?? 0.0 + let withdrawAmount: UFix64 = 0.3 + log("Withdrawing 0.3 PYUSD0 from vault \(pyusd0VaultID)...") + Test.expect( + _executeTransactionFile("../transactions/flow-yield-vaults/withdraw_from_yield_vault.cdc", [pyusd0VaultID, withdrawAmount], [pyusd0User]), + Test.beSucceeded() + ) + let after = (_executeScript("../scripts/flow-yield-vaults/get_yield_vault_balance.cdc", [pyusd0User.address, pyusd0VaultID]).returnValue! as! UFix64?)! + Test.assert(equalAmounts(a: after, b: before - withdrawAmount, tolerance: (before - withdrawAmount) * tolerancePct), + message: "PYUSD0 withdraw: expected ~\(before - withdrawAmount), got \(after)") + log("PYUSD0 vault balance after withdrawal: \(after)") +} + +access(all) fun testCloseSyWFLOWvYieldVault_PYUSD0() { + let vaultBalBefore = (_executeScript("../scripts/flow-yield-vaults/get_yield_vault_balance.cdc", [pyusd0User.address, pyusd0VaultID]).returnValue! as! UFix64?) ?? 0.0 + let userBalBefore = _pyusd0Balance(pyusd0User) + log("Closing PYUSD0 vault \(pyusd0VaultID) (balance: \(vaultBalBefore), user PYUSD0: \(userBalBefore))...") + Test.expect( + _executeTransactionFile("../transactions/flow-yield-vaults/close_yield_vault.cdc", [pyusd0VaultID], [pyusd0User]), + Test.beSucceeded() + ) + let vaultBalAfter = _executeScript("../scripts/flow-yield-vaults/get_yield_vault_balance.cdc", [pyusd0User.address, pyusd0VaultID]) + Test.expect(vaultBalAfter, Test.beSucceeded()) + Test.assert(vaultBalAfter.returnValue == nil, message: "PYUSD0 vault should no longer exist after close") + let userBalAfter = _pyusd0Balance(pyusd0User) + let returned = userBalAfter - userBalBefore + Test.assert(equalAmounts(a: returned, b: vaultBalBefore, tolerance: vaultBalBefore * tolerancePct), + message: "Expected ~\(vaultBalBefore) PYUSD0 returned on close, got \(returned)") + log("PYUSD0 yield vault closed — returned \(returned) PYUSD0 (vault had \(vaultBalBefore))") +} + +/* ========================================================= + WBTC collateral lifecycle + ========================================================= */ + +access(all) fun testCreateSyWFLOWvYieldVault_WBTC() { + let collateralAmount: UFix64 = 0.0001 + log("Creating syWFLOWvStrategy yield vault with \(collateralAmount) WBTC...") + let result = _executeTransactionFile( + "../transactions/flow-yield-vaults/create_yield_vault.cdc", + [syWFLOWvStrategyIdentifier, wbtcVaultIdentifier, collateralAmount], + [wbtcUser] + ) + Test.expect(result, Test.beSucceeded()) + + wbtcVaultID = _latestVaultID(wbtcUser) + log("Created WBTC vault ID: \(wbtcVaultID)") + + 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(equalAmounts(a: balance!, b: collateralAmount, tolerance: collateralAmount * tolerancePct), + message: "WBTC vault balance after create should be ~\(collateralAmount), got \(balance!)") + log("WBTC vault balance after create: \(balance!)") +} + +access(all) fun testDepositToSyWFLOWvYieldVault_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 \(wbtcVaultID)...") + 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: (before + depositAmount) * tolerancePct), + message: "WBTC deposit: expected ~\(before + depositAmount), got \(after)") + log("WBTC vault balance after deposit: \(after)") +} + +access(all) fun testWithdrawFromSyWFLOWvYieldVault_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 \(wbtcVaultID)...") + 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: (before - withdrawAmount) * tolerancePct), + message: "WBTC withdraw: expected ~\(before - withdrawAmount), got \(after)") + log("WBTC vault balance after withdrawal: \(after)") +} + +access(all) fun testCloseSyWFLOWvYieldVault_WBTC() { + let vaultBalBefore = (_executeScript("../scripts/flow-yield-vaults/get_yield_vault_balance.cdc", [wbtcUser.address, wbtcVaultID]).returnValue! as! UFix64?) ?? 0.0 + let userBalBefore = _wbtcBalance(wbtcUser) + log("Closing WBTC vault \(wbtcVaultID) (balance: \(vaultBalBefore), user WBTC: \(userBalBefore))...") + 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") + let userBalAfter = _wbtcBalance(wbtcUser) + let returned = userBalAfter - userBalBefore + Test.assert(equalAmounts(a: returned, b: vaultBalBefore, tolerance: vaultBalBefore * tolerancePct), + message: "Expected ~\(vaultBalBefore) WBTC returned on close, got \(returned)") + log("WBTC yield vault closed — returned \(returned) WBTC (vault had \(vaultBalBefore))") +} + +/* ========================================================= + WETH collateral lifecycle + ========================================================= */ + +access(all) fun testCreateSyWFLOWvYieldVault_WETH() { + let collateralAmount: UFix64 = 0.001 + log("Creating syWFLOWvStrategy yield vault with \(collateralAmount) WETH...") + let result = _executeTransactionFile( + "../transactions/flow-yield-vaults/create_yield_vault.cdc", + [syWFLOWvStrategyIdentifier, wethVaultIdentifier, collateralAmount], + [wethUser] + ) + Test.expect(result, Test.beSucceeded()) + + wethVaultID = _latestVaultID(wethUser) + log("Created WETH vault ID: \(wethVaultID)") + + 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(equalAmounts(a: balance!, b: collateralAmount, tolerance: collateralAmount * tolerancePct), + message: "WETH vault balance after create should be ~\(collateralAmount), got \(balance!)") + log("WETH vault balance after create: \(balance!)") +} + +access(all) fun testDepositToSyWFLOWvYieldVault_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 \(wethVaultID)...") + 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: (before + depositAmount) * tolerancePct), + message: "WETH deposit: expected ~\(before + depositAmount), got \(after)") + log("WETH vault balance after deposit: \(after)") +} + +access(all) fun testWithdrawFromSyWFLOWvYieldVault_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 \(wethVaultID)...") + 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: (before - withdrawAmount) * tolerancePct), + message: "WETH withdraw: expected ~\(before - withdrawAmount), got \(after)") + log("WETH vault balance after withdrawal: \(after)") +} + +access(all) fun testCloseSyWFLOWvYieldVault_WETH() { + let vaultBalBefore = (_executeScript("../scripts/flow-yield-vaults/get_yield_vault_balance.cdc", [wethUser.address, wethVaultID]).returnValue! as! UFix64?) ?? 0.0 + let userBalBefore = _wethBalance(wethUser) + log("Closing WETH vault \(wethVaultID) (balance: \(vaultBalBefore), user WETH: \(userBalBefore))...") + 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") + let userBalAfter = _wethBalance(wethUser) + let returned = userBalAfter - userBalBefore + Test.assert(equalAmounts(a: returned, b: vaultBalBefore, tolerance: vaultBalBefore * tolerancePct), + message: "Expected ~\(vaultBalBefore) WETH returned on close, got \(returned)") + log("WETH yield vault closed — returned \(returned) WETH (vault had \(vaultBalBefore))") +} + +/* ========================================================= + Negative tests + ========================================================= */ + +/// FLOW is the underlying / debt asset of syWFLOWvStrategy — it must be rejected as collateral. +access(all) fun testCannotCreateYieldVaultWithFLOWAsCollateral() { + let flowVaultIdentifier = "A.1654653399040a61.FlowToken.Vault" + log("Attempting to create syWFLOWvStrategy vault with FLOW (debt asset) as collateral — expecting failure...") + let result = _executeTransactionFile( + "../transactions/flow-yield-vaults/create_yield_vault.cdc", + [syWFLOWvStrategyIdentifier, flowVaultIdentifier, 1.0], + [pyusd0User] + ) + Test.expect(result, Test.beFailed()) + log("Correctly rejected FLOW 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 WETH vault, then attempt to deposit WBTC into it — the strategy pre-condition should panic. +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", + [syWFLOWvStrategyIdentifier, wethVaultIdentifier, 0.001], + [wethUser] + ) + Test.expect(createResult, Test.beSucceeded()) + let freshWethVaultID = _latestVaultID(wethUser) + log("Created WETH vault ID: \(freshWethVaultID) — 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)") +} + +/* ========================================================= + Excess-yield test + ========================================================= */ + +/// Opens a syWFLOWvStrategy PYUSD0 vault, injects extra syWFLOWv to create an excess scenario, +/// closes the vault, and verifies that the excess is returned as PYUSD0 to the user. +/// +/// Using PYUSD0 as collateral makes the excess-return clearly visible: the user ends up with +/// more PYUSD0 than they started with, because the injected syWFLOWv shares are converted back +/// to PYUSD0 (via syWFLOWv → FLOW → PYUSD0) and added to the returned collateral. +/// +/// Scenario: +/// 1. Open a syWFLOWvStrategy vault with 2.0 PYUSD0. +/// 2. Convert 50 FLOW → syWFLOWv and deposit directly into the AutoBalancer. +/// (pyusd0User already holds FLOW on mainnet — no setup needed.) +/// → AutoBalancer balance now exceeds what is needed to repay the FLOW debt. +/// 3. Close the vault. +/// → Step 8 of closePosition() drains the remaining syWFLOWv, converts it to +/// PYUSD0 (syWFLOWv → FLOW → PYUSD0), and adds it to the returned collateral. +/// 4. Verify pyusd0After > pyusd0Before: the user gained net PYUSD0 from the excess. +access(all) fun testCloseSyWFLOWvVaultWithExcessYieldTokens_PYUSD0() { + log("=== testCloseSyWFLOWvVaultWithExcessYieldTokens_PYUSD0 ===") + + let pyusd0Before = _pyusd0Balance(pyusd0User) + log("PYUSD0 balance before vault creation: \(pyusd0Before)") + + let collateralAmount: UFix64 = 2.0 + log("Creating syWFLOWvStrategy vault with \(collateralAmount) PYUSD0...") + let createResult = _executeTransactionFile( + "../transactions/flow-yield-vaults/create_yield_vault.cdc", + [syWFLOWvStrategyIdentifier, pyusd0VaultIdentifier, collateralAmount], + [pyusd0User] + ) + Test.expect(createResult, Test.beSucceeded()) + + let vaultID = _latestVaultID(pyusd0User) + log("Created vault ID: \(vaultID)") + + let vaultBalAfterCreate = _executeScript( + "../scripts/flow-yield-vaults/get_yield_vault_balance.cdc", + [pyusd0User.address, vaultID] + ) + Test.expect(vaultBalAfterCreate, Test.beSucceeded()) + let vaultBal = vaultBalAfterCreate.returnValue! as! UFix64? + Test.assert(equalAmounts(a: vaultBal!, b: collateralAmount, tolerance: collateralAmount * tolerancePct), + message: "Expected vault balance ~\(collateralAmount) after create, got: \(vaultBal ?? 0.0)") + log("Vault balance (PYUSD0 collateral value): \(vaultBal!)") + + let abBalBefore = _autoBalancerBalance(vaultID) + Test.assert(abBalBefore! > 0.0, + message: "Expected positive AutoBalancer balance after vault creation, got: \(abBalBefore ?? 0.0)") + log("AutoBalancer syWFLOWv balance before injection: \(abBalBefore!)") + + // pyusd0User holds FLOW on mainnet — inject directly without any setup. + let injectionFlowAmount: UFix64 = 10.0 + log("Injecting \(injectionFlowAmount) FLOW worth of syWFLOWv into AutoBalancer...") + let injectResult = _executeTransactionFile( + "transactions/inject_flow_as_sywflowv_to_autobalancer.cdc", + [vaultID, syWFLOWvEVMAddress, injectionFlowAmount], + [pyusd0User] + ) + Test.expect(injectResult, Test.beSucceeded()) + + let abBalAfter = _autoBalancerBalance(vaultID) + Test.assert(abBalAfter != nil, + message: "AutoBalancer should still exist after injection") + Test.assert(abBalAfter! > abBalBefore!, + message: "AutoBalancer balance should have increased after injection. Before: \(abBalBefore!) After: \(abBalAfter!)") + let injectedShares = abBalAfter! - abBalBefore! + log("AutoBalancer syWFLOWv balance after injection: \(abBalAfter!)") + log("Injected \(injectedShares) syWFLOWv shares (excess over original debt coverage)") + + log("Closing vault \(vaultID)...") + let closeResult = _executeTransactionFile( + "../transactions/flow-yield-vaults/close_yield_vault.cdc", + [vaultID], + [pyusd0User] + ) + Test.expect(closeResult, Test.beSucceeded()) + + let vaultBalAfterClose = _executeScript( + "../scripts/flow-yield-vaults/get_yield_vault_balance.cdc", + [pyusd0User.address, vaultID] + ) + Test.expect(vaultBalAfterClose, Test.beSucceeded()) + Test.assert(vaultBalAfterClose.returnValue == nil, + message: "Vault \(vaultID) should not exist after close") + log("Vault no longer exists — close confirmed") + + let abBalFinal = _autoBalancerBalance(vaultID) + Test.assert(abBalFinal == nil, + message: "AutoBalancer should be nil (burned) after vault close, but got: \(abBalFinal ?? 0.0)") + log("AutoBalancer is nil after close — torn down during _cleanupAutoBalancer") + + let pyusd0After = _pyusd0Balance(pyusd0User) + log("PYUSD0 balance after close: \(pyusd0After)") + + // 10 FLOW ≈ $0.3–0.5 at current prices — well above tx fees incurred during this test. + // The net gain should be clearly positive: excess syWFLOWv → FLOW → PYUSD0 adds more + // PYUSD0 back than the transactions consume in fees. + Test.assert( + pyusd0After > pyusd0Before, + message: "User should have more PYUSD0 than before (excess syWFLOWv converted back to PYUSD0). Before: \(pyusd0Before), After: \(pyusd0After)" + ) + let pyusd0Net = pyusd0After - pyusd0Before + log("Net PYUSD0 gain from excess syWFLOWv conversion: \(pyusd0Net) PYUSD0 (injected ~\(injectionFlowAmount) FLOW worth)") + + log("=== testCloseSyWFLOWvVaultWithExcessYieldTokens_PYUSD0 PASSED ===") +} diff --git a/cadence/tests/fork/flow_yield_vaults_backward_compat_test.cdc b/cadence/tests/fork/flow_yield_vaults_backward_compat_test.cdc index b7bafa39..071ba13e 100644 --- a/cadence/tests/fork/flow_yield_vaults_backward_compat_test.cdc +++ b/cadence/tests/fork/flow_yield_vaults_backward_compat_test.cdc @@ -85,12 +85,10 @@ access(all) fun setup() { path: "../../contracts/FlowYieldVaultsSchedulerV1.cdc", arguments: [] ), - - // @TODO restore in strategies PR - // ContractSpec( - // path: "../../contracts/FlowYieldVaultsStrategiesV2.cdc", - // arguments: [univ3Factory, univ3Router, univ3Quoter] - // ), + ContractSpec( + path: "../../contracts/FlowYieldVaultsStrategiesV2.cdc", + arguments: [univ3Factory, univ3Router, univ3Quoter] + ), ContractSpec( path: "../../contracts/PMStrategiesV1.cdc", arguments: [univ3Factory, univ3Router, univ3Quoter] diff --git a/cadence/tests/transactions/inject_flow_as_sywflowv_to_autobalancer.cdc b/cadence/tests/transactions/inject_flow_as_sywflowv_to_autobalancer.cdc new file mode 100644 index 00000000..8f8cb7f4 --- /dev/null +++ b/cadence/tests/transactions/inject_flow_as_sywflowv_to_autobalancer.cdc @@ -0,0 +1,71 @@ +import "FungibleToken" +import "FlowToken" +import "EVM" +import "FungibleTokenConnectors" +import "ERC4626SwapConnectors" +import "FlowYieldVaultsAutoBalancersV1" + +/// Converts FLOW → syWFLOWv (via ERC4626 deposit on the More vault) and injects the +/// resulting shares directly into the AutoBalancer for the given yield vault ID. +/// +/// This simulates accumulated yield in the AutoBalancer, producing a scenario where +/// the yield token (syWFLOWv) balance is greater than the vault's outstanding FLOW debt. +/// +/// The signer must hold FLOW in their /storage/flowTokenVault and have a COA at /storage/evm. +/// +/// @param vaultID: The YieldVault ID whose AutoBalancer should receive extra syWFLOWv +/// @param syWFLOWvEVMAddr: EVM address of the syWFLOWv ERC4626 vault (hex with 0x prefix) +/// @param flowAmount: Amount of FLOW to convert to syWFLOWv and inject +/// +transaction( + vaultID: UInt64, + syWFLOWvEVMAddr: String, + flowAmount: UFix64 +) { + prepare(signer: auth(Storage, BorrowValue, IssueStorageCapabilityController) &Account) { + // Issue a COA capability with EVM.Call and EVM.Bridge entitlements for the ERC4626 deposit + let coaCap = signer.capabilities.storage.issue(/storage/evm) + + // Issue a withdraw capability on the signer's FLOW vault to serve as the fee source + let flowWithdrawCap = signer.capabilities.storage.issue(/storage/flowTokenVault) + let feeSource = FungibleTokenConnectors.VaultSinkAndSource( + min: nil, + max: nil, + vault: flowWithdrawCap, + uniqueID: nil + ) + + // Build ERC4626 swapper: FLOW (Cadence) → syWFLOWv (ERC4626 deposit via WFLOW bridge) + // asset = FlowToken.Vault whose EVM counterpart is WFLOW — the underlying of syWFLOWv + let swapper = ERC4626SwapConnectors.Swapper( + asset: Type<@FlowToken.Vault>(), + vault: EVM.addressFromString(syWFLOWvEVMAddr), + coa: coaCap, + feeSource: feeSource, + uniqueID: nil + ) + + // Get a quote for converting flowAmount FLOW → syWFLOWv shares + let quote = swapper.quoteOut(forProvided: flowAmount, reverse: false) + assert( + quote.outAmount > 0.0, + message: "FLOW → syWFLOWv quote returned zero — syWFLOWv vault may be at capacity" + ) + + // Withdraw FLOW from signer's vault + let flowVault = signer.storage.borrow(from: /storage/flowTokenVault) + ?? panic("No FLOW vault found at /storage/flowTokenVault") + let flowIn <- flowVault.withdraw(amount: flowAmount) + + // Swap FLOW → syWFLOWv (bridges to EVM, deposits into syWFLOWv, bridges back to Cadence) + let syWFLOWvOut <- swapper.swap(quote: quote, inVault: <-flowIn) + log("Converted ".concat(flowAmount.toString()).concat(" FLOW → ").concat(syWFLOWvOut.balance.toString()).concat(" syWFLOWv shares")) + + // Deposit the syWFLOWv shares directly into the vault's AutoBalancer. + // AutoBalancers.AutoBalancer.deposit() is access(all) — callable via the public reference. + let autoBalancer = FlowYieldVaultsAutoBalancersV1.borrowAutoBalancer(id: vaultID) + ?? panic("No AutoBalancer found for vault ID ".concat(vaultID.toString())) + autoBalancer.deposit(from: <-syWFLOWvOut) + log("Injected syWFLOWv into AutoBalancer for vault ID ".concat(vaultID.toString())) + } +} diff --git a/cadence/tests/transactions/inject_pyusd0_as_fusdev_to_autobalancer.cdc b/cadence/tests/transactions/inject_pyusd0_as_fusdev_to_autobalancer.cdc new file mode 100644 index 00000000..44e34980 --- /dev/null +++ b/cadence/tests/transactions/inject_pyusd0_as_fusdev_to_autobalancer.cdc @@ -0,0 +1,71 @@ +import "FungibleToken" +import "FlowToken" +import "EVM" +import "FungibleTokenConnectors" +import "MorphoERC4626SwapConnectors" +import "FlowYieldVaultsAutoBalancersV1" + +/// Converts PYUSD0 → FUSDEV (via Morpho ERC4626 deposit) and injects the resulting +/// shares directly into the AutoBalancer for the given yield vault ID. +/// +/// This simulates accumulated yield in the AutoBalancer, producing a scenario where +/// the yield token (FUSDEV) balance is greater than the vault's outstanding PYUSD0 debt. +/// +/// The signer must hold PYUSD0 in their Cadence vault and have a COA at /storage/evm. +/// FLOW at /storage/flowTokenVault is used for bridge fees. +/// +/// @param vaultID: The YieldVault ID whose AutoBalancer should receive FUSDEV +/// @param fusdEvEVMAddr: EVM address of the FUSDEV Morpho ERC4626 vault (hex with 0x prefix) +/// @param pyusd0Amount: Amount of PYUSD0 to deposit into FUSDEV and inject as excess +/// +transaction( + vaultID: UInt64, + fusdEvEVMAddr: String, + pyusd0Amount: UFix64 +) { + prepare(signer: auth(Storage, BorrowValue, IssueStorageCapabilityController) &Account) { + // Issue COA capability (needs EVM.Call and EVM.Bridge for Morpho ERC4626 deposit) + let coaCap = signer.capabilities.storage.issue(/storage/evm) + + // Issue a withdraw capability on the signer's FLOW vault to serve as the bridge fee source + let flowWithdrawCap = signer.capabilities.storage.issue(/storage/flowTokenVault) + let feeSource = FungibleTokenConnectors.VaultSinkAndSource( + min: nil, + max: nil, + vault: flowWithdrawCap, + uniqueID: nil + ) + + // Build Morpho ERC4626 swapper: PYUSD0 → FUSDEV (isReversed: false) + // The Swapper auto-detects PYUSD0 as the underlying asset from the FUSDEV vault. + let swapper = MorphoERC4626SwapConnectors.Swapper( + vaultEVMAddress: EVM.addressFromString(fusdEvEVMAddr), + coa: coaCap, + feeSource: feeSource, + uniqueID: nil, + isReversed: false + ) + + let pyusd0Provider = signer.storage.borrow( + from: /storage/EVMVMBridgedToken_99af3eea856556646c98c8b9b2548fe815240750Vault + ) ?? panic("No PYUSD0 vault found") + + let pyusd0In <- pyusd0Provider.withdraw(amount: pyusd0Amount) + + // Quote and swap PYUSD0 → FUSDEV (Morpho ERC4626 deposit via EVM) + let quote = swapper.quoteOut(forProvided: pyusd0Amount, reverse: false) + assert( + quote.outAmount > 0.0, + message: "PYUSD0 → FUSDEV quote returned zero — Morpho vault may be at capacity" + ) + let fusdEvOut <- swapper.swap(quote: quote, inVault: <-pyusd0In) + log("Converted ".concat(pyusd0Amount.toString()).concat(" PYUSD0 → ").concat(fusdEvOut.balance.toString()).concat(" FUSDEV shares")) + + // Deposit FUSDEV shares directly into the vault's AutoBalancer. + // AutoBalancers.AutoBalancer.deposit() is access(all) — callable via the public reference. + let autoBalancer = FlowYieldVaultsAutoBalancersV1.borrowAutoBalancer(id: vaultID) + ?? panic("No AutoBalancer found for vault ID ".concat(vaultID.toString())) + autoBalancer.deposit(from: <-fusdEvOut) + log("Injected FUSDEV into AutoBalancer for vault ID ".concat(vaultID.toString())) + } +} diff --git a/cadence/tests/transactions/provision_wbtc_from_weth.cdc b/cadence/tests/transactions/provision_wbtc_from_weth.cdc index 0f9289d9..bc7bedc3 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) diff --git a/cadence/tests/transactions/swap_flow_to_pyusd0.cdc b/cadence/tests/transactions/swap_flow_to_pyusd0.cdc deleted file mode 100644 index 8637a277..00000000 --- a/cadence/tests/transactions/swap_flow_to_pyusd0.cdc +++ /dev/null @@ -1,159 +0,0 @@ -import "FungibleToken" -import "FungibleTokenMetadataViews" -import "ViewResolver" -import "FlowToken" -import "EVM" -import "FlowEVMBridgeUtils" -import "FlowEVMBridgeConfig" -import "ScopedFTProviders" - -/// Funds the signer with PYUSD0 by swapping FLOW on EVM via UniswapV3, then bridging to Cadence. -/// -/// Steps: -/// 1. Deposit FLOW to the signer's COA -/// 2. Wrap FLOW to WFLOW -/// 3. Swap WFLOW -> PYUSD0 via UniswapV3 exactInput -/// 4. Bridge PYUSD0 from EVM to Cadence -/// -/// @param flowAmount: Amount of FLOW to swap for PYUSD0 -/// -transaction(flowAmount: UFix64) { - - let coa: auth(EVM.Owner, EVM.Call, EVM.Bridge) &EVM.CadenceOwnedAccount - let scopedProvider: @ScopedFTProviders.ScopedFTProvider - let receiver: &{FungibleToken.Vault} - - prepare(signer: auth(BorrowValue, SaveValue, IssueStorageCapabilityController, PublishCapability, UnpublishCapability, CopyValue) &Account) { - // Borrow COA - self.coa = signer.storage.borrow(from: /storage/evm) - ?? panic("No COA at /storage/evm") - - // Withdraw FLOW and deposit to COA - let flowVault = signer.storage.borrow(from: /storage/flowTokenVault) - ?? panic("No FlowToken vault") - let deposit <- flowVault.withdraw(amount: flowAmount) as! @FlowToken.Vault - self.coa.deposit(from: <-deposit) - - // Set up scoped fee provider for bridging - let approxFee = FlowEVMBridgeUtils.calculateBridgeFee(bytes: 400_000) - if signer.storage.type(at: FlowEVMBridgeConfig.providerCapabilityStoragePath) == nil { - let providerCap = signer.capabilities.storage.issue( - /storage/flowTokenVault - ) - signer.storage.save(providerCap, to: FlowEVMBridgeConfig.providerCapabilityStoragePath) - } - let providerCapCopy = signer.storage.copy>( - from: FlowEVMBridgeConfig.providerCapabilityStoragePath - ) ?? panic("Invalid provider capability") - self.scopedProvider <- ScopedFTProviders.createScopedFTProvider( - provider: providerCapCopy, - filters: [ScopedFTProviders.AllowanceFilter(approxFee)], - expiration: getCurrentBlock().timestamp + 1000.0 - ) - - // Set up PYUSD0 vault if needed - let pyusd0Type = CompositeType("A.1e4aa0b87d10b141.EVMVMBridgedToken_99af3eea856556646c98c8b9b2548fe815240750.Vault")! - let tokenContractAddress = FlowEVMBridgeUtils.getContractAddress(fromType: pyusd0Type) - ?? panic("Could not get PYUSD0 contract address") - let tokenContractName = FlowEVMBridgeUtils.getContractName(fromType: pyusd0Type) - ?? panic("Could not get PYUSD0 contract name") - let viewResolver = getAccount(tokenContractAddress).contracts.borrow<&{ViewResolver}>(name: tokenContractName) - ?? panic("Could not borrow ViewResolver for PYUSD0") - let vaultData = viewResolver.resolveContractView( - resourceType: pyusd0Type, - viewType: Type() - ) as! FungibleTokenMetadataViews.FTVaultData? - ?? panic("Could not resolve FTVaultData for PYUSD0") - if signer.storage.borrow<&{FungibleToken.Vault}>(from: vaultData.storagePath) == nil { - signer.storage.save(<-vaultData.createEmptyVault(), to: vaultData.storagePath) - signer.capabilities.unpublish(vaultData.receiverPath) - signer.capabilities.unpublish(vaultData.metadataPath) - let receiverCap = signer.capabilities.storage.issue<&{FungibleToken.Vault}>(vaultData.storagePath) - let metadataCap = signer.capabilities.storage.issue<&{FungibleToken.Vault}>(vaultData.storagePath) - signer.capabilities.publish(receiverCap, at: vaultData.receiverPath) - signer.capabilities.publish(metadataCap, at: vaultData.metadataPath) - } - self.receiver = signer.storage.borrow<&{FungibleToken.Vault}>(from: vaultData.storagePath) - ?? panic("Could not borrow PYUSD0 vault") - } - - execute { - let wflowAddr = EVM.addressFromString("0xd3bF53DAC106A0290B0483EcBC89d40FcC961f3e") - let pyusd0Addr = EVM.addressFromString("0x99aF3EeA856556646C98c8B9b2548Fe815240750") - let routerAddr = EVM.addressFromString("0xeEDC6Ff75e1b10B903D9013c358e446a73d35341") - let zeroValue = EVM.Balance(attoflow: 0) - - // 1. Wrap FLOW -> WFLOW - let flowBalance = EVM.Balance(attoflow: 0) - flowBalance.setFLOW(flow: flowAmount) - let wrapRes = self.coa.call( - to: wflowAddr, - data: EVM.encodeABIWithSignature("deposit()", []), - gasLimit: 100_000, - value: flowBalance - ) - assert(wrapRes.status == EVM.Status.successful, message: "WFLOW wrap failed: ".concat(wrapRes.errorMessage)) - - // 2. Approve UniV3 Router to spend WFLOW - let amountEVM = FlowEVMBridgeUtils.convertCadenceAmountToERC20Amount(flowAmount, erc20Address: wflowAddr) - let approveRes = self.coa.call( - to: wflowAddr, - data: EVM.encodeABIWithSignature("approve(address,uint256)", [routerAddr, amountEVM]), - gasLimit: 100_000, - value: zeroValue - ) - assert(approveRes.status == EVM.Status.successful, message: "WFLOW approve failed: ".concat(approveRes.errorMessage)) - - // 3. Swap WFLOW -> PYUSD0 via UniV3 exactInput - // Path encoding: tokenIn(20) | fee(3 bytes big-endian) | tokenOut(20) - var pathBytes: [UInt8] = [] - let wflowFixed: [UInt8; 20] = wflowAddr.bytes - let pyusd0Fixed: [UInt8; 20] = pyusd0Addr.bytes - var i = 0 - while i < 20 { pathBytes.append(wflowFixed[i]); i = i + 1 } - // fee 3000 = 0x000BB8 big-endian - pathBytes.append(0x00) - pathBytes.append(0x0B) - pathBytes.append(0xB8) - i = 0 - while i < 20 { pathBytes.append(pyusd0Fixed[i]); i = i + 1 } - - let swapRes = self.coa.call( - to: routerAddr, - data: EVM.encodeABIWithSignature( - "exactInput((bytes,address,uint256,uint256))", - [EVM.EVMBytes(value: pathBytes), self.coa.address(), amountEVM, UInt256(0)] - ), - gasLimit: 1_000_000, - value: zeroValue - ) - assert(swapRes.status == EVM.Status.successful, message: "UniV3 swap failed: ".concat(swapRes.errorMessage)) - - // 4. Check PYUSD0 balance in COA - let balRes = self.coa.call( - to: pyusd0Addr, - data: EVM.encodeABIWithSignature("balanceOf(address)", [self.coa.address()]), - gasLimit: 100_000, - value: zeroValue - ) - assert(balRes.status == EVM.Status.successful, message: "balanceOf failed") - let decoded = EVM.decodeABI(types: [Type()], data: balRes.data) - let pyusd0Balance = decoded[0] as! UInt256 - assert(pyusd0Balance > UInt256(0), message: "No PYUSD0 received from swap") - - // 5. Bridge PYUSD0 from EVM to Cadence - let pyusd0Type = CompositeType("A.1e4aa0b87d10b141.EVMVMBridgedToken_99af3eea856556646c98c8b9b2548fe815240750.Vault")! - let bridgedVault <- self.coa.withdrawTokens( - type: pyusd0Type, - amount: pyusd0Balance, - feeProvider: &self.scopedProvider as auth(FungibleToken.Withdraw) &{FungibleToken.Provider} - ) - assert(bridgedVault.balance > 0.0, message: "Bridged PYUSD0 vault is empty") - log("Bridged PYUSD0 amount: ".concat(bridgedVault.balance.toString())) - - // Deposit bridged PYUSD0 into the signer's Cadence vault - self.receiver.deposit(from: <-bridgedVault) - - destroy self.scopedProvider - } -} diff --git a/cadence/tests/transactions/transfer_pyusd0.cdc b/cadence/tests/transactions/transfer_pyusd0.cdc new file mode 100644 index 00000000..6508f25a --- /dev/null +++ b/cadence/tests/transactions/transfer_pyusd0.cdc @@ -0,0 +1,47 @@ +import "FungibleToken" +import "FungibleTokenMetadataViews" +import "ViewResolver" + +/// Transfers PYUSD0 from the first signer (sender) to the second signer (receiver). +/// Sets up the receiver's PYUSD0 Cadence vault if it is not already present. +/// +/// Used in tests to provision PYUSD0 to accounts that have a COA but no PYUSD0, +/// avoiding the need for an EVM swap. +/// +/// @param amount: PYUSD0 amount (UFix64) to transfer +transaction(amount: UFix64) { + + let vault: @{FungibleToken.Vault} + let receiver: &{FungibleToken.Vault} + + prepare( + sender: auth(BorrowValue) &Account, + rcvr: auth(Storage, IssueStorageCapabilityController, PublishCapability, UnpublishCapability) &Account + ) { + let storagePath = /storage/EVMVMBridgedToken_99af3eea856556646c98c8b9b2548fe815240750Vault + + self.vault <- (sender.storage.borrow(from: storagePath) + ?? panic("Sender has no PYUSD0 vault")).withdraw(amount: amount) + + // Set up receiver's PYUSD0 vault if not present + if rcvr.storage.borrow<&{FungibleToken.Vault}>(from: storagePath) == nil { + let pyusd0Type = CompositeType("A.1e4aa0b87d10b141.EVMVMBridgedToken_99af3eea856556646c98c8b9b2548fe815240750.Vault")! + let vaultData = getAccount(pyusd0Type.address!).contracts.borrow<&{ViewResolver}>(name: pyusd0Type.contractName!)! + .resolveContractView(resourceType: pyusd0Type, viewType: Type()) + as! FungibleTokenMetadataViews.FTVaultData? + ?? panic("Could not resolve FTVaultData for PYUSD0") + rcvr.storage.save(<-vaultData.createEmptyVault(), to: storagePath) + rcvr.capabilities.unpublish(vaultData.receiverPath) + rcvr.capabilities.unpublish(vaultData.metadataPath) + rcvr.capabilities.publish(rcvr.capabilities.storage.issue<&{FungibleToken.Vault}>(storagePath), at: vaultData.receiverPath) + rcvr.capabilities.publish(rcvr.capabilities.storage.issue<&{FungibleToken.Vault}>(storagePath), at: vaultData.metadataPath) + } + self.receiver = rcvr.storage.borrow<&{FungibleToken.Vault}>(from: storagePath) + ?? panic("Could not borrow receiver PYUSD0 vault") + } + + execute { + self.receiver.deposit(from: <-self.vault) + log("Transferred ".concat(amount.toString()).concat(" PYUSD0 to receiver")) + } +} diff --git a/cadence/transactions/flow-yield-vaults/admin/remove_strategy_composer.cdc b/cadence/transactions/flow-yield-vaults/admin/remove_strategy_composer.cdc new file mode 100644 index 00000000..1f6ab1f1 --- /dev/null +++ b/cadence/transactions/flow-yield-vaults/admin/remove_strategy_composer.cdc @@ -0,0 +1,30 @@ +import "FlowYieldVaults" + +/// Removes a Strategy type from the FlowYieldVaults StrategyFactory. +/// +/// Use this to clean up stale or broken strategy entries — for example, strategies whose +/// backing contract no longer type-checks against the current FlowYieldVaults.Strategy interface. +/// +/// @param strategyIdentifier: The Type identifier of the Strategy to remove, e.g. +/// "A.d2580caf2ef07c2f.FlowYieldVaultsStrategies.TracerStrategy" +/// +transaction(strategyIdentifier: String) { + + let factory: auth(Mutate) &FlowYieldVaults.StrategyFactory + + prepare(signer: auth(BorrowValue) &Account) { + self.factory = signer.storage.borrow( + from: FlowYieldVaults.FactoryStoragePath + ) ?? panic("Could not borrow StrategyFactory from \(FlowYieldVaults.FactoryStoragePath)") + } + + execute { + let strategyType = CompositeType(strategyIdentifier) + ?? panic("Invalid strategy type identifier: \(strategyIdentifier)") + let removed = self.factory.removeStrategy(strategyType) + log(removed + ? "Removed \(strategyIdentifier) from StrategyFactory" + : "Strategy \(strategyIdentifier) was not found in StrategyFactory" + ) + } +} diff --git a/cadence/transactions/flow-yield-vaults/admin/upsert_more_erc4626_config.cdc b/cadence/transactions/flow-yield-vaults/admin/upsert_more_erc4626_config.cdc new file mode 100644 index 00000000..e1d75bd2 --- /dev/null +++ b/cadence/transactions/flow-yield-vaults/admin/upsert_more_erc4626_config.cdc @@ -0,0 +1,69 @@ +import "FungibleToken" +import "EVM" +import "FlowYieldVaultsStrategiesV2" + +/// Admin tx to configure a MoreERC4626CollateralConfig entry for a strategy in FlowYieldVaultsStrategiesV2. +/// +/// Used for strategies that borrow the ERC4626 vault's own underlying asset directly +/// (e.g. syWFLOWvStrategy: collateral → borrow FLOW → deposit into More ERC4626 vault). +/// +/// Must be signed by the account that deployed FlowYieldVaultsStrategiesV2. +transaction( + /// e.g. "A.0x...FlowYieldVaultsStrategiesV2.syWFLOWvStrategy" + strategyTypeIdentifier: String, + + /// collateral vault type (e.g. "A.0x...SomeToken.Vault") + tokenTypeIdentifier: String, + + /// yield token EVM address (the More ERC4626 vault, e.g. syWFLOWv) + yieldTokenEVMAddress: String, + + /// UniV3 path for yield token → underlying (e.g. [syWFLOWv, WFLOW]) + yieldToUnderlyingPath: [String], + yieldToUnderlyingFees: [UInt32], + + /// UniV3 path for debt token → collateral (used for dust conversion on close) + debtToCollateralPath: [String], + debtToCollateralFees: [UInt32] +) { + let issuer: auth(FlowYieldVaultsStrategiesV2.Configure) &FlowYieldVaultsStrategiesV2.StrategyComposerIssuer + let strategyType: Type + let tokenType: Type + let yieldTokenEVMAddr: EVM.EVMAddress + let yieldToUnderlyingAddressPath: [EVM.EVMAddress] + let debtToCollateralAddressPath: [EVM.EVMAddress] + + prepare(acct: auth(Storage) &Account) { + self.strategyType = CompositeType(strategyTypeIdentifier) + ?? panic("Invalid strategyTypeIdentifier \(strategyTypeIdentifier)") + self.tokenType = CompositeType(tokenTypeIdentifier) + ?? panic("Invalid tokenTypeIdentifier \(tokenTypeIdentifier)") + + self.issuer = acct.storage.borrow< + auth(FlowYieldVaultsStrategiesV2.Configure) &FlowYieldVaultsStrategiesV2.StrategyComposerIssuer + >(from: FlowYieldVaultsStrategiesV2.IssuerStoragePath) + ?? panic("Missing StrategyComposerIssuer at IssuerStoragePath") + + fun toEVM(_ hexes: [String]): [EVM.EVMAddress] { + let out: [EVM.EVMAddress] = [] + for h in hexes { out.append(EVM.addressFromString(h)) } + return out + } + + self.yieldTokenEVMAddr = EVM.addressFromString(yieldTokenEVMAddress) + self.yieldToUnderlyingAddressPath = toEVM(yieldToUnderlyingPath) + self.debtToCollateralAddressPath = toEVM(debtToCollateralPath) + } + + execute { + self.issuer.addOrUpdateMoreERC4626CollateralConfig( + strategyType: self.strategyType, + collateralVaultType: self.tokenType, + yieldTokenEVMAddress: self.yieldTokenEVMAddr, + yieldToUnderlyingAddressPath: self.yieldToUnderlyingAddressPath, + yieldToUnderlyingFeePath: yieldToUnderlyingFees, + debtToCollateralAddressPath: self.debtToCollateralAddressPath, + debtToCollateralFeePath: debtToCollateralFees + ) + } +} diff --git a/flow.json b/flow.json index 12c8046f..1b4b4fe5 100644 --- a/flow.json +++ b/flow.json @@ -1201,16 +1201,6 @@ }, "mainnet": { "mainnet-admin": [ - { - "name": "MockOracle", - "args": [ - { - "value": "A.6b00ff876c299c61.MOET.Vault", - "type": "String" - } - ] - }, - "MockSwapper", "UInt64LinkedList", "AutoBalancers", "FlowYieldVaultsSchedulerRegistry", diff --git a/lib/FlowALP b/lib/FlowALP index cded789d..aa10eff7 160000 --- a/lib/FlowALP +++ b/lib/FlowALP @@ -1 +1 @@ -Subproject commit cded789d07d9d3d92914036533d93c6b0a0417cd +Subproject commit aa10eff7bfd7a0e351b37ff9ef79a106003d66be diff --git a/local/setup_mainnet.sh b/local/setup_mainnet.sh index 6509f167..ff684e18 100755 --- a/local/setup_mainnet.sh +++ b/local/setup_mainnet.sh @@ -22,7 +22,7 @@ flow transactions send ./lib/FlowALP/cadence/transactions/flow-alp/pool-factory/ flow transactions send ./lib/FlowALP/cadence/transactions/flow-alp/pool-governance/update_oracle.cdc --network mainnet --signer mainnet-flow-alp-deployer # add FLOW as supported token - params: collateralFactor, borrowFactor, depositRate, depositCapacityCap -flow transactions send ./lib/FlowALP/cadence/transactions/flow-alp/pool-governance/add_supported_token_simple_interest_curve.cdc \ +flow transactions send ./lib/FlowALP/cadence/transactions/flow-alp/pool-governance/add_supported_token_zero_rate_curve.cdc \ 'A.1654653399040a61.FlowToken.Vault' \ 0.8 \ 1.0 \ @@ -34,11 +34,8 @@ flow transactions send ./lib/FlowALP/cadence/transactions/flow-alp/pool-governan # add WBTC to band oracle cd ./lib/FlowALP/FlowActions && flow transactions send ./cadence/transactions/band-oracle-connector/add_symbol.cdc "BTC" "A.1e4aa0b87d10b141.EVMVMBridgedToken_717dae2baf7656be9a9b01dee31d571a9d4c9579.Vault" --network mainnet --signer mainnet-band-oracle-connectors && cd ../../.. -# add WETH to band oracle -cd ./lib/FlowALP/FlowActions && flow transactions send ./cadence/transactions/band-oracle-connector/add_symbol.cdc "ETH" "A.1e4aa0b87d10b141.EVMVMBridgedToken_2f6f07cdcf3588944bf4c42ac74ff24bf56e7590.Vault" --network mainnet --signer mainnet-band-oracle-connectors && cd ../../.. - # WBTC simple curve -flow transactions send ./lib/FlowALP/cadence/transactions/flow-alp/pool-governance/add_supported_token_simple_interest_curve.cdc \ +flow transactions send ./lib/FlowALP/cadence/transactions/flow-alp/pool-governance/add_supported_token_zero_rate_curve.cdc \ 'A.1e4aa0b87d10b141.EVMVMBridgedToken_717dae2baf7656be9a9b01dee31d571a9d4c9579.Vault' \ 0.8 \ 1.0 \ @@ -47,8 +44,18 @@ flow transactions send ./lib/FlowALP/cadence/transactions/flow-alp/pool-governan --network mainnet \ --signer mainnet-flow-alp-deployer +# set minimum deposit for WBTC ~ 0.005 USD +flow transactions send ./lib/FlowALP/cadence/transactions/flow-alp/pool-governance/set_minimum_token_balance_per_position.cdc \ + 'A.1e4aa0b87d10b141.EVMVMBridgedToken_717dae2baf7656be9a9b01dee31d571a9d4c9579.Vault' \ + 0.0000001 \ + --network mainnet \ + --signer mainnet-flow-alp-deployer + +# add WETH to band oracle +cd ./lib/FlowALP/FlowActions && flow transactions send ./cadence/transactions/band-oracle-connector/add_symbol.cdc "ETH" "A.1e4aa0b87d10b141.EVMVMBridgedToken_2f6f07cdcf3588944bf4c42ac74ff24bf56e7590.Vault" --network mainnet --signer mainnet-band-oracle-connectors && cd ../../.. + # WETH simple curve -flow transactions send ./lib/FlowALP/cadence/transactions/flow-alp/pool-governance/add_supported_token_simple_interest_curve.cdc \ +flow transactions send ./lib/FlowALP/cadence/transactions/flow-alp/pool-governance/add_supported_token_zero_rate_curve.cdc \ 'A.1e4aa0b87d10b141.EVMVMBridgedToken_2f6f07cdcf3588944bf4c42ac74ff24bf56e7590.Vault' \ 0.8 \ 1.0 \ @@ -57,6 +64,33 @@ flow transactions send ./lib/FlowALP/cadence/transactions/flow-alp/pool-governan --network mainnet \ --signer mainnet-flow-alp-deployer +# set minimum deposit for WETH ~ 0.01 USD +flow transactions send ./lib/FlowALP/cadence/transactions/flow-alp/pool-governance/set_minimum_token_balance_per_position.cdc \ + 'A.1e4aa0b87d10b141.EVMVMBridgedToken_2f6f07cdcf3588944bf4c42ac74ff24bf56e7590.Vault' \ + 0.00001 \ + --network mainnet \ + --signer mainnet-flow-alp-deployer + +# add PYUSD0 to band oracle +cd ./lib/FlowALP/FlowActions && flow transactions send ./cadence/transactions/band-oracle-connector/add_symbol.cdc "PYUSD" "A.1e4aa0b87d10b141.EVMVMBridgedToken_99af3eea856556646c98c8b9b2548fe815240750.Vault" --network mainnet --signer mainnet-band-oracle-connectors && cd ../../.. + +# PYUSD0 simple curve +flow transactions send ./lib/FlowALP/cadence/transactions/flow-alp/pool-governance/add_supported_token_zero_rate_curve.cdc \ + 'A.1e4aa0b87d10b141.EVMVMBridgedToken_99af3eea856556646c98c8b9b2548fe815240750.Vault' \ + 0.8 \ + 1.0 \ + 1_000_000.0 \ + 1_000_000.0 \ + --network mainnet \ + --signer mainnet-flow-alp-deployer + +# set minimum deposit for PYUSD0 ~ 0.01 USD +flow transactions send ./lib/FlowALP/cadence/transactions/flow-alp/pool-governance/set_minimum_token_balance_per_position.cdc \ + 'A.1e4aa0b87d10b141.EVMVMBridgedToken_99af3eea856556646c98c8b9b2548fe815240750.Vault' \ + 0.01 \ + --network mainnet \ + --signer mainnet-flow-alp-deployer + # kink interest curve setup # enable when FCM_V1 is deployed # @@ -145,7 +179,6 @@ flow transactions send ./cadence/transactions/flow-yield-vaults/admin/upsert_str '[100,3000]' \ --network mainnet \ --signer mainnet-admin -# flow transactions send ./cadence/transactions/flow-yield-vaults/admin/add_strategy_composer.cdc \ 'A.b1d63873c3cc9f79.FlowYieldVaultsStrategiesV2.FUSDEVStrategy' \ @@ -154,19 +187,56 @@ flow transactions send ./cadence/transactions/flow-yield-vaults/admin/add_strate --network mainnet \ --signer mainnet-admin -# configure PMStrategies strategy configs -flow transactions send ./cadence/transactions/flow-yield-vaults/admin/upsert-pm-strategy-config.cdc \ - 'A.b1d63873c3cc9f79.PMStrategiesV1.syWFLOWvStrategy' \ - 'A.1654653399040a61.FlowToken.Vault' \ +# configure syWFLOWvStrategy (MoreERC4626) collateral configs +# +# PYUSD0: yieldToUnderlying = syWFLOWv→WFLOW (fee 100), debtToCollateral = WFLOW→PYUSD0 (fee 3000) +flow transactions send ./cadence/transactions/flow-yield-vaults/admin/upsert_more_erc4626_config.cdc \ + 'A.b1d63873c3cc9f79.FlowYieldVaultsStrategiesV2.syWFLOWvStrategy' \ + 'A.1e4aa0b87d10b141.EVMVMBridgedToken_99af3eea856556646c98c8b9b2548fe815240750.Vault' \ '0xCBf9a7753F9D2d0e8141ebB36d99f87AcEf98597' \ - 100 \ + '["0xCBf9a7753F9D2d0e8141ebB36d99f87AcEf98597","0xd3bF53DAC106A0290B0483EcBC89d40FcC961f3e"]' \ + '[100]' \ + '["0xd3bF53DAC106A0290B0483EcBC89d40FcC961f3e","0x99aF3EeA856556646C98c8B9b2548Fe815240750"]' \ + '[3000]' \ + --network mainnet \ + --signer mainnet-admin + +# WBTC: no WFLOW/WBTC pool — use 2-hop WFLOW→WETH→WBTC (fees 3000/3000) +flow transactions send ./cadence/transactions/flow-yield-vaults/admin/upsert_more_erc4626_config.cdc \ + 'A.b1d63873c3cc9f79.FlowYieldVaultsStrategiesV2.syWFLOWvStrategy' \ + 'A.1e4aa0b87d10b141.EVMVMBridgedToken_717dae2baf7656be9a9b01dee31d571a9d4c9579.Vault' \ + '0xCBf9a7753F9D2d0e8141ebB36d99f87AcEf98597' \ + '["0xCBf9a7753F9D2d0e8141ebB36d99f87AcEf98597","0xd3bF53DAC106A0290B0483EcBC89d40FcC961f3e"]' \ + '[100]' \ + '["0xd3bF53DAC106A0290B0483EcBC89d40FcC961f3e","0x2F6F07CDcf3588944Bf4C42aC74ff24bF56e7590","0x717DAE2BaF7656BE9a9B01deE31d571a9d4c9579"]' \ + '[3000,3000]' \ --network mainnet \ --signer mainnet-admin +# WETH: yieldToUnderlying = syWFLOWv→WFLOW (fee 100), debtToCollateral = WFLOW→WETH (fee 3000) +flow transactions send ./cadence/transactions/flow-yield-vaults/admin/upsert_more_erc4626_config.cdc \ + 'A.b1d63873c3cc9f79.FlowYieldVaultsStrategiesV2.syWFLOWvStrategy' \ + 'A.1e4aa0b87d10b141.EVMVMBridgedToken_2f6f07cdcf3588944bf4c42ac74ff24bf56e7590.Vault' \ + '0xCBf9a7753F9D2d0e8141ebB36d99f87AcEf98597' \ + '["0xCBf9a7753F9D2d0e8141ebB36d99f87AcEf98597","0xd3bF53DAC106A0290B0483EcBC89d40FcC961f3e"]' \ + '[100]' \ + '["0xd3bF53DAC106A0290B0483EcBC89d40FcC961f3e","0x2F6F07CDcf3588944Bf4C42aC74ff24bF56e7590"]' \ + '[3000]' \ + --network mainnet \ + --signer mainnet-admin + +flow transactions send ./cadence/transactions/flow-yield-vaults/admin/add_strategy_composer.cdc \ + 'A.b1d63873c3cc9f79.FlowYieldVaultsStrategiesV2.syWFLOWvStrategy' \ + 'A.b1d63873c3cc9f79.FlowYieldVaultsStrategiesV2.MoreERC4626StrategyComposer' \ + /storage/FlowYieldVaultsStrategyV2ComposerIssuer_0xb1d63873c3cc9f79 \ + --network mainnet \ + --signer mainnet-admin + +# configure PMStrategies strategy configs flow transactions send ./cadence/transactions/flow-yield-vaults/admin/upsert-pm-strategy-config.cdc \ - 'A.b1d63873c3cc9f79.PMStrategiesV1.tauUSDFvStrategy' \ - 'A.1e4aa0b87d10b141.EVMVMBridgedToken_2aabea2058b5ac2d339b163c6ab6f2b6d53aabed.Vault' \ - '0xc52E820d2D6207D18667a97e2c6Ac22eB26E803c' \ + 'A.b1d63873c3cc9f79.PMStrategiesV1.syWFLOWvStrategy' \ + 'A.1654653399040a61.FlowToken.Vault' \ + '0xCBf9a7753F9D2d0e8141ebB36d99f87AcEf98597' \ 100 \ --network mainnet \ --signer mainnet-admin @@ -186,13 +256,6 @@ flow transactions send ./cadence/transactions/flow-yield-vaults/admin/add_strate --network mainnet \ --signer mainnet-admin -flow transactions send ./cadence/transactions/flow-yield-vaults/admin/add_strategy_composer.cdc \ - 'A.b1d63873c3cc9f79.PMStrategiesV1.tauUSDFvStrategy' \ - 'A.b1d63873c3cc9f79.PMStrategiesV1.ERC4626VaultStrategyComposer' \ - /storage/PMStrategiesV1ComposerIssuer_0xb1d63873c3cc9f79 \ - --network mainnet \ - --signer mainnet-admin - flow transactions send ./cadence/transactions/flow-yield-vaults/admin/add_strategy_composer.cdc \ 'A.b1d63873c3cc9f79.PMStrategiesV1.FUSDEVStrategy' \ 'A.b1d63873c3cc9f79.PMStrategiesV1.ERC4626VaultStrategyComposer' \ @@ -222,7 +285,10 @@ flow transactions send ./lib/FlowALP/cadence/tests/transactions/flow-alp/pool-ma # --proposer # test FlowYieldVault strategy - +# +# FUSDEV Strategy +# +# WFLOW (FLOW) # flow transactions send ./cadence/transactions/flow-yield-vaults/create_yield_vault.cdc \ # A.b1d63873c3cc9f79.FlowYieldVaultsStrategiesV2.FUSDEVStrategy \ # A.1654653399040a61.FlowToken.Vault \ @@ -231,7 +297,6 @@ flow transactions send ./lib/FlowALP/cadence/tests/transactions/flow-alp/pool-ma # --network mainnet \ # --signer # -# # WBTC # flow transactions send ./cadence/transactions/flow-yield-vaults/create_yield_vault.cdc \ # A.b1d63873c3cc9f79.FlowYieldVaultsStrategiesV2.FUSDEVStrategy \ @@ -250,6 +315,52 @@ flow transactions send ./lib/FlowALP/cadence/tests/transactions/flow-alp/pool-ma # --network mainnet \ # --signer # +# PYUSD0 - should fail +# flow transactions send ./cadence/transactions/flow-yield-vaults/create_yield_vault.cdc \ +# A.b1d63873c3cc9f79.FlowYieldVaultsStrategiesV2.FUSDEVStrategy \ +# A.1e4aa0b87d10b141.EVMVMBridgedToken_99af3eea856556646c98c8b9b2548fe815240750.Vault \ +# 0.01 \ +# --compute-limit 9999 \ +# --network mainnet \ +# --signer +# +# syWFLOWv Strategy +# +# WFLOW (FLOW) - should fail +# flow transactions send ./cadence/transactions/flow-yield-vaults/create_yield_vault.cdc \ +# A.b1d63873c3cc9f79.FlowYieldVaultsStrategiesV2.syWFLOWvStrategy \ +# A.1654653399040a61.FlowToken.Vault \ +# 1.0 \ +# --compute-limit 9999 \ +# --network mainnet \ +# --signer alex +# +# WBTC (BTCf) +# flow transactions send ./cadence/transactions/flow-yield-vaults/create_yield_vault.cdc \ +# A.b1d63873c3cc9f79.FlowYieldVaultsStrategiesV2.syWFLOWvStrategy \ +# A.1e4aa0b87d10b141.EVMVMBridgedToken_717dae2baf7656be9a9b01dee31d571a9d4c9579.Vault \ +# 0.0000001 \ +# --compute-limit 9999 \ +# --network mainnet \ +# --signer +# +# WETH (ETHf) +# flow transactions send ./cadence/transactions/flow-yield-vaults/create_yield_vault.cdc \ +# A.b1d63873c3cc9f79.FlowYieldVaultsStrategiesV2.syWFLOWvStrategy \ +# A.1e4aa0b87d10b141.EVMVMBridgedToken_2f6f07cdcf3588944bf4c42ac74ff24bf56e7590.Vault \ +# 0.00001 \ +# --compute-limit 9999 \ +# --network mainnet \ +# --signer +# +# PYUSD0 +# flow transactions send ./cadence/transactions/flow-yield-vaults/create_yield_vault.cdc \ +# A.b1d63873c3cc9f79.FlowYieldVaultsStrategiesV2.syWFLOWvStrategy \ +# A.1e4aa0b87d10b141.EVMVMBridgedToken_99af3eea856556646c98c8b9b2548fe815240750.Vault \ +# 0.01 \ +# --compute-limit 9999 \ +# --network mainnet \ +# --signer # # test PEAK MONEY strategy # diff --git a/local/setup_testnet.sh b/local/setup_testnet.sh index b185a199..0de795e6 100755 --- a/local/setup_testnet.sh +++ b/local/setup_testnet.sh @@ -4,6 +4,21 @@ git submodule update --init --recursive flow deps install --skip-alias --skip-deployments flow project deploy --network testnet --update +# Remove the stale FlowYieldVaultsStrategies.TracerStrategy from the StrategyFactory. +# +# The old FlowYieldVaultsStrategies contract has TracerStrategy that no longer conforms to +# FlowYieldVaults.Strategy (missing closePosition). This blocks deserialization of the entire +# StrategyFactory, causing createYieldVault to fail. The stub deployed above re-establishes +# conformance; this call removes the stale entry. +flow transactions send ./cadence/transactions/flow-yield-vaults/admin/remove_strategy_composer.cdc \ + 'A.d2580caf2ef07c2f.FlowYieldVaultsStrategies.TracerStrategy' \ + --network testnet --signer testnet-admin + +# Remove MockStrategies.TracerStrategy as well (test-only; not needed in production). +flow transactions send ./cadence/transactions/flow-yield-vaults/admin/remove_strategy_composer.cdc \ + 'A.d2580caf2ef07c2f.MockStrategies.TracerStrategy' \ + --network testnet --signer testnet-admin + # set mocked prices in the MockOracle contract, initialized with MOET as unitOfAccount flow transactions send ./cadence/transactions/mocks/oracle/set_price.cdc 'A.7e60df042a9c0868.FlowToken.Vault' 0.5 --network testnet --signer testnet-admin flow transactions send ./cadence/transactions/mocks/oracle/set_price.cdc 'A.d2580caf2ef07c2f.YieldToken.Vault' 1.0 --network testnet --signer testnet-admin @@ -47,6 +62,13 @@ flow transactions send ./lib/FlowALP/cadence/transactions/flow-alp/pool-governan --network testnet \ --signer testnet-flow-alp-deployer +# set minimum deposit for WBTC +flow transactions send ./lib/FlowALP/cadence/transactions/flow-alp/pool-governance/set_minimum_token_balance_per_position.cdc \ + 'A.dfc20aee650fcbdf.EVMVMBridgedToken_208d09d2a6dd176e3e95b3f0de172a7471c5b2d6.Vault' \ + 0.0001 \ + --network testnet \ + --signer testnet-flow-alp-deployer + # add WETH as supported token flow transactions send ./lib/FlowALP/cadence/transactions/flow-alp/pool-governance/add_supported_token_simple_interest_curve.cdc \ 'A.dfc20aee650fcbdf.EVMVMBridgedToken_059a77239dafa770977dd9f1e98632c3e4559848.Vault' \ @@ -57,6 +79,13 @@ flow transactions send ./lib/FlowALP/cadence/transactions/flow-alp/pool-governan --network testnet \ --signer testnet-flow-alp-deployer +# set minimum deposit for WETH +flow transactions send ./lib/FlowALP/cadence/transactions/flow-alp/pool-governance/set_minimum_token_balance_per_position.cdc \ + 'A.dfc20aee650fcbdf.EVMVMBridgedToken_059a77239dafa770977dd9f1e98632c3e4559848.Vault' \ + 0.001 \ + --network testnet \ + --signer testnet-flow-alp-deployer + echo "swap Flow to MOET" flow transactions send ./lib/FlowALP/cadence/transactions/flow-alp/create_position.cdc 100000.0 --network testnet --signer testnet-flow-alp-deployer @@ -90,38 +119,50 @@ flow transactions send ./cadence/transactions/flow-yield-vaults/admin/add_strate ## PYUSD0 Vault # WFLOW univ3 path and fees -# path: FUSDEV - WFLOW +# path: FUSDEV - PYUSD0 - WFLOW flow transactions send ./cadence/transactions/flow-yield-vaults/admin/upsert_strategy_config.cdc \ 'A.d2580caf2ef07c2f.FlowYieldVaultsStrategiesV2.FUSDEVStrategy' \ 'A.7e60df042a9c0868.FlowToken.Vault' \ "0x61b44D19486EE492449E83C1201581C754e9e1E1" \ - '["0x61b44D19486EE492449E83C1201581C754e9e1E1", "0xd3bF53DAC106A0290B0483EcBC89d40FcC961f3e"]' \ - '[3000]' \ + '["0x61b44D19486EE492449E83C1201581C754e9e1E1", "0xd7d43ab7b365f0d0789ae83f4385fa710ffdc98f", "0xd3bF53DAC106A0290B0483EcBC89d40FcC961f3e"]' \ + '[100, 3000]' \ --network testnet \ --signer testnet-admin # WETH univ3 path and fees -# path: FUSDEV - MOET - WETH +# path: FUSDEV - PYUSD0 - WETH flow transactions send ./cadence/transactions/flow-yield-vaults/admin/upsert_strategy_config.cdc \ 'A.d2580caf2ef07c2f.FlowYieldVaultsStrategiesV2.FUSDEVStrategy' \ 'A.dfc20aee650fcbdf.EVMVMBridgedToken_059a77239dafa770977dd9f1e98632c3e4559848.Vault' \ "0x61b44D19486EE492449E83C1201581C754e9e1E1" \ - '["0x61b44D19486EE492449E83C1201581C754e9e1E1","0x02d3575e2516a515E9B91a52b294Edc80DC7987c", "0x059A77239daFa770977DD9f1E98632C3E4559848"]' \ - '[3000,3000]' \ + '["0x61b44D19486EE492449E83C1201581C754e9e1E1","0xd7d43ab7b365f0d0789ae83f4385fa710ffdc98f", "0x059A77239daFa770977DD9f1E98632C3E4559848"]' \ + '[100,3000]' \ --network testnet \ --signer testnet-admin # WBTC univ3 path and fees -# path: FUSDEV - MOET - WETH +# path: FUSDEV - PYUSD0 - WBTC flow transactions send ./cadence/transactions/flow-yield-vaults/admin/upsert_strategy_config.cdc \ 'A.d2580caf2ef07c2f.FlowYieldVaultsStrategiesV2.FUSDEVStrategy' \ 'A.dfc20aee650fcbdf.EVMVMBridgedToken_208d09d2a6dd176e3e95b3f0de172a7471c5b2d6.Vault' \ "0x61b44D19486EE492449E83C1201581C754e9e1E1" \ - '["0x61b44D19486EE492449E83C1201581C754e9e1E1","0x02d3575e2516a515E9B91a52b294Edc80DC7987c","0x208d09d2a6Dd176e3e95b3F0DE172A7471C5B2d6"]' \ - '[3000,3000]' \ + '["0x61b44D19486EE492449E83C1201581C754e9e1E1","0xd7d43ab7b365f0d0789ae83f4385fa710ffdc98f","0x208d09d2a6Dd176e3e95b3F0DE172A7471C5B2d6"]' \ + '[100,3000]' \ --network testnet \ --signer testnet-admin +# PYUSD0 univ3 path and fees +# path: FUSDEV - PYUSD0 (fee 100, stable pool) +# testnet PYUSD0 EVM: 0xd7d43ab7b365f0d0789ae83f4385fa710ffdc98f +# flow transactions send ./cadence/transactions/flow-yield-vaults/admin/upsert_strategy_config.cdc \ +# 'A.d2580caf2ef07c2f.FlowYieldVaultsStrategiesV2.FUSDEVStrategy' \ +# 'A.dfc20aee650fcbdf.EVMVMBridgedToken_d7d43ab7b365f0d0789ae83f4385fa710ffdc98f.Vault' \ +# "0x61b44D19486EE492449E83C1201581C754e9e1E1" \ +# '["0x61b44D19486EE492449E83C1201581C754e9e1E1","0xd7d43ab7b365f0d0789ae83f4385fa710ffdc98f"]' \ +# '[100]' \ +# --network testnet \ +# --signer testnet-admin + flow transactions send ./cadence/transactions/flow-yield-vaults/admin/add_strategy_composer.cdc \ 'A.d2580caf2ef07c2f.FlowYieldVaultsStrategiesV2.FUSDEVStrategy' \ 'A.d2580caf2ef07c2f.FlowYieldVaultsStrategiesV2.MorphoERC4626StrategyComposer' \ @@ -129,6 +170,66 @@ flow transactions send ./cadence/transactions/flow-yield-vaults/admin/add_strate --network testnet \ --signer testnet-admin +# configure FlowYieldVaultsStrategiesV2 syWFLOWvStrategy +# +# UniswapV3 addresses (testnet): factory=0x92657b195e22b69E4779BBD09Fa3CD46F0CF8e39 +# EVM tokens (testnet): +# WFLOW: 0xd3bF53DAC106A0290B0483EcBC89d40FcC961f3e +# WBTC: 0x208d09d2a6Dd176e3e95b3F0DE172A7471C5B2d6 +# WETH: 0x059A77239daFa770977DD9f1E98632C3E4559848 +# PYUSD0: 0xd7d43ab7b365f0d0789ae83f4385fa710ffdc98f +# +# TODO: fill in testnet syWFLOWv More ERC4626 vault address (mainnet: 0xCBf9a7753F9D2d0e8141ebB36d99f87AcEf98597) +# +# yieldToUnderlying path is the same for all collaterals: syWFLOWv → WFLOW (fee 100, 0.01%) +# debtToCollateral paths differ per collateral: WFLOW → +# testnet uses PYUSD0 as the intermediate hop (mirrors testnet FUSDEVStrategy pool structure) + +# WBTC collateral — syWFLOWv → WFLOW (fee 100), WFLOW → PYUSD0 → WBTC (fees 3000/3000) +flow transactions send ./cadence/transactions/flow-yield-vaults/admin/upsert_more_erc4626_config.cdc \ + 'A.d2580caf2ef07c2f.FlowYieldVaultsStrategiesV2.syWFLOWvStrategy' \ + 'A.dfc20aee650fcbdf.EVMVMBridgedToken_208d09d2a6dd176e3e95b3f0de172a7471c5b2d6.Vault' \ + '0x' \ + '["0x","0xd3bF53DAC106A0290B0483EcBC89d40FcC961f3e"]' \ + '[100]' \ + '["0xd3bF53DAC106A0290B0483EcBC89d40FcC961f3e","0xd7d43ab7b365f0d0789ae83f4385fa710ffdc98f","0x208d09d2a6Dd176e3e95b3F0DE172A7471C5B2d6"]' \ + '[3000,3000]' \ + --network testnet \ + --signer testnet-admin + +# WETH collateral — syWFLOWv → WFLOW (fee 100), WFLOW → PYUSD0 → WETH (fees 3000/3000) +flow transactions send ./cadence/transactions/flow-yield-vaults/admin/upsert_more_erc4626_config.cdc \ + 'A.d2580caf2ef07c2f.FlowYieldVaultsStrategiesV2.syWFLOWvStrategy' \ + 'A.dfc20aee650fcbdf.EVMVMBridgedToken_059a77239dafa770977dd9f1e98632c3e4559848.Vault' \ + '0x' \ + '["0x","0xd3bF53DAC106A0290B0483EcBC89d40FcC961f3e"]' \ + '[100]' \ + '["0xd3bF53DAC106A0290B0483EcBC89d40FcC961f3e","0xd7d43ab7b365f0d0789ae83f4385fa710ffdc98f","0x059A77239daFa770977DD9f1E98632C3E4559848"]' \ + '[3000,3000]' \ + --network testnet \ + --signer testnet-admin + +# PYUSD0 collateral — syWFLOWv → WFLOW (fee 100), WFLOW → PYUSD0 (fee 500) +# TODO: verify WFLOW/PYUSD0 pool fee on testnet (mainnet uses 500) +flow transactions send ./cadence/transactions/flow-yield-vaults/admin/upsert_more_erc4626_config.cdc \ + 'A.d2580caf2ef07c2f.FlowYieldVaultsStrategiesV2.syWFLOWvStrategy' \ + 'A.dfc20aee650fcbdf.EVMVMBridgedToken_d7d43ab7b365f0d0789ae83f4385fa710ffdc98f.Vault' \ + '0x' \ + '["0x","0xd3bF53DAC106A0290B0483EcBC89d40FcC961f3e"]' \ + '[100]' \ + '["0xd3bF53DAC106A0290B0483EcBC89d40FcC961f3e","0xd7d43ab7b365f0d0789ae83f4385fa710ffdc98f"]' \ + '[500]' \ + --network testnet \ + --signer testnet-admin + +# register syWFLOWvStrategy in FlowYieldVaults factory +flow transactions send ./cadence/transactions/flow-yield-vaults/admin/add_strategy_composer.cdc \ + 'A.d2580caf2ef07c2f.FlowYieldVaultsStrategiesV2.syWFLOWvStrategy' \ + 'A.d2580caf2ef07c2f.FlowYieldVaultsStrategiesV2.MoreERC4626StrategyComposer' \ + /storage/FlowYieldVaultsStrategyV2ComposerIssuer_0xd2580caf2ef07c2f \ + --network testnet \ + --signer testnet-admin + # PYUSD0 Vault flow transactions send ./cadence/transactions/flow-yield-vaults/admin/upsert-pm-strategy-config.cdc \ 'A.d2580caf2ef07c2f.PMStrategiesV1.FUSDEVStrategy' \