From 3be4c4c51649520a5018525b39cd43f807a2c3c9 Mon Sep 17 00:00:00 2001 From: Alex <12097569+nialexsan@users.noreply.github.com> Date: Tue, 24 Feb 2026 10:19:37 -0500 Subject: [PATCH 01/27] deploy strategies v2 to mainnet --- local/setup_mainnet.sh | 61 ++++++++++++++++++++++++++++-------------- 1 file changed, 41 insertions(+), 20 deletions(-) diff --git a/local/setup_mainnet.sh b/local/setup_mainnet.sh index cd7fe499..43b67b8a 100755 --- a/local/setup_mainnet.sh +++ b/local/setup_mainnet.sh @@ -34,9 +34,6 @@ 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 \ 'A.1e4aa0b87d10b141.EVMVMBridgedToken_717dae2baf7656be9a9b01dee31d571a9d4c9579.Vault' \ @@ -47,6 +44,16 @@ 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 \ 'A.1e4aa0b87d10b141.EVMVMBridgedToken_2f6f07cdcf3588944bf4c42ac74ff24bf56e7590.Vault' \ @@ -57,6 +64,35 @@ 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 + +# TODO: setup PYUSD0 + +# # 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_simple_interest_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 # @@ -163,14 +199,6 @@ flow transactions send ./cadence/transactions/flow-yield-vaults/admin/upsert-pm- --network mainnet \ --signer mainnet-admin -flow transactions send ./cadence/transactions/flow-yield-vaults/admin/upsert-pm-strategy-config.cdc \ - 'A.b1d63873c3cc9f79.PMStrategiesV1.tauUSDFvStrategy' \ - 'A.1e4aa0b87d10b141.EVMVMBridgedToken_2aabea2058b5ac2d339b163c6ab6f2b6d53aabed.Vault' \ - '0xc52E820d2D6207D18667a97e2c6Ac22eB26E803c' \ - 100 \ - --network mainnet \ - --signer mainnet-admin - flow transactions send ./cadence/transactions/flow-yield-vaults/admin/upsert-pm-strategy-config.cdc \ 'A.b1d63873c3cc9f79.PMStrategiesV1.FUSDEVStrategy' \ 'A.1e4aa0b87d10b141.EVMVMBridgedToken_99af3eea856556646c98c8b9b2548fe815240750.Vault' \ @@ -186,13 +214,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 +243,8 @@ flow transactions send ./lib/FlowALP/cadence/tests/transactions/flow-alp/pool-ma # --proposer # test FlowYieldVault 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 +253,6 @@ flow transactions send ./lib/FlowALP/cadence/tests/transactions/flow-alp/pool-ma # --network mainnet \ # --signer # -# # WBTC (BTCf) # flow transactions send ./cadence/transactions/flow-yield-vaults/create_yield_vault.cdc \ # A.b1d63873c3cc9f79.FlowYieldVaultsStrategiesV2.FUSDEVStrategy \ From df6272579495db39699f3e8162c274e7a570bba0 Mon Sep 17 00:00:00 2001 From: Alex <12097569+nialexsan@users.noreply.github.com> Date: Tue, 24 Feb 2026 11:47:40 -0500 Subject: [PATCH 02/27] add pyusd --- cadence/contracts/mocks/MockStrategies.cdc | 2 +- .../scripts/band-oracle/get_pyusd_price.cdc | 37 +++++++++++++ .../band-oracle/get_pyusd_price.cdc | 36 +++++++++++++ local/setup_mainnet.sh | 53 +++++++++++-------- 4 files changed, 104 insertions(+), 24 deletions(-) create mode 100644 cadence/scripts/band-oracle/get_pyusd_price.cdc create mode 100644 cadence/transactions/band-oracle/get_pyusd_price.cdc diff --git a/cadence/contracts/mocks/MockStrategies.cdc b/cadence/contracts/mocks/MockStrategies.cdc index 84fbdcc1..e073780c 100644 --- a/cadence/contracts/mocks/MockStrategies.cdc +++ b/cadence/contracts/mocks/MockStrategies.cdc @@ -330,7 +330,7 @@ access(all) contract MockStrategies { } init() { - self.IssuerStoragePath = StoragePath(identifier: "FlowYieldVaultsStrategyComposerIssuer_\(self.account.address)")! + self.IssuerStoragePath = StoragePath(identifier: "MockStrategyComposerIssuer_\(self.account.address)")! let initialCollateralType = Type<@FlowToken.Vault>() diff --git a/cadence/scripts/band-oracle/get_pyusd_price.cdc b/cadence/scripts/band-oracle/get_pyusd_price.cdc new file mode 100644 index 00000000..a454ed37 --- /dev/null +++ b/cadence/scripts/band-oracle/get_pyusd_price.cdc @@ -0,0 +1,37 @@ +import "FungibleToken" +import "FlowToken" +import "BandOracle" + +/// Retrieves the PYUSD/USD price from the Band Protocol oracle on Flow. +/// +/// BandOracle stores rates as symbol/USD values and computes cross-rates on demand. +/// Querying PYUSD/USD returns the USD price of one PYUSD token (~1.0 for a healthy peg). +/// +/// NOTE: BandOracle.getReferenceData requires a FLOW fee payment. This script creates an +/// empty vault and succeeds only when BandOracle.getFee() == 0.0. If the fee is non-zero, +/// use the get_pyusd_price transaction instead, which withdraws from the signer's FLOW vault. +/// +/// @return A struct with: +/// - fixedPointRate: UFix64 — PYUSD/USD price as a decimal (e.g. 0.99980000) +/// - integerE18Rate: UInt256 — rate multiplied by 10^18 +/// - baseTimestamp: UInt64 — UNIX epoch of the last PYUSD data update on BandChain +/// - quoteTimestamp: UInt64 — UNIX epoch of the last USD data update on BandChain +/// +access(all) +fun main(): BandOracle.ReferenceData { + let fee = BandOracle.getFee() + assert(fee == 0.0, message: "BandOracle fee is non-zero (\(fee) FLOW). Use the get_pyusd_price transaction to pay the fee.") + + // Create an empty vault satisfying the payment parameter (fee == 0.0 is already asserted above) + let payment <- FlowToken.createEmptyVault(vaultType: Type<@FlowToken.Vault>()) + + // PYUSD is the base symbol; USD is the implicit quote for all Band oracle rates. + // The returned fixedPointRate = PYUSD price in USD. + let priceData = BandOracle.getReferenceData( + baseSymbol: "USD", + quoteSymbol: "USD", + payment: <-payment + ) + + return priceData +} diff --git a/cadence/transactions/band-oracle/get_pyusd_price.cdc b/cadence/transactions/band-oracle/get_pyusd_price.cdc new file mode 100644 index 00000000..2f0ec4bd --- /dev/null +++ b/cadence/transactions/band-oracle/get_pyusd_price.cdc @@ -0,0 +1,36 @@ +import "FungibleToken" +import "FlowToken" +import "BandOracle" + +/// Retrieves the PYUSD/USD price from the Band Protocol oracle, paying the oracle fee from +/// the signer's FLOW vault. Use this transaction when BandOracle.getFee() > 0.0. +/// +/// The price is emitted to the transaction log. Band oracle rates are USD-denominated, so +/// PYUSD/USD returns the USD value of one PYUSD token (~1.0 for a healthy peg). +/// +/// Excess FLOW (payment beyond the required fee) is returned to the signer's vault. +/// +transaction { + + prepare(signer: auth(BorrowValue) &Account) { + let fee = BandOracle.getFee() + + // Borrow the signer's FLOW vault and withdraw the exact oracle fee + let flowVault = signer.storage.borrow( + from: /storage/flowTokenVault + ) ?? panic("Could not borrow signer's FlowToken vault") + + let payment <- flowVault.withdraw(amount: fee) as! @FlowToken.Vault + + let priceData = BandOracle.getReferenceData( + baseSymbol: "PYUSD", + quoteSymbol: "USD", + payment: <-payment + ) + + log("PYUSD/USD price (UFix64): ".concat(priceData.fixedPointRate.toString())) + log("PYUSD/USD rate (e18 integer): ".concat(priceData.integerE18Rate.toString())) + log("Base timestamp (UNIX): ".concat(priceData.baseTimestamp.toString())) + log("Quote timestamp (UNIX): ".concat(priceData.quoteTimestamp.toString())) + } +} diff --git a/local/setup_mainnet.sh b/local/setup_mainnet.sh index 43b67b8a..261f5cc0 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 \ @@ -35,7 +35,7 @@ flow transactions send ./lib/FlowALP/cadence/transactions/flow-alp/pool-governan 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 ../../.. # 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 \ @@ -55,7 +55,7 @@ flow transactions send ./lib/FlowALP/cadence/transactions/flow-alp/pool-governan 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 \ @@ -71,27 +71,25 @@ flow transactions send ./lib/FlowALP/cadence/transactions/flow-alp/pool-governan --network mainnet \ --signer mainnet-flow-alp-deployer -# TODO: setup PYUSD0 +# 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 ../../.. -# # 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_simple_interest_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 +# 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 @@ -182,6 +180,15 @@ flow transactions send ./cadence/transactions/flow-yield-vaults/admin/upsert_str --network mainnet \ --signer mainnet-admin # +# Setup UniV3 path FUSDEV -> PYUSD0 +flow transactions send ./cadence/transactions/flow-yield-vaults/admin/upsert_strategy_config.cdc \ + 'A.b1d63873c3cc9f79.FlowYieldVaultsStrategiesV2.FUSDEVStrategy' \ + 'A.1e4aa0b87d10b141.EVMVMBridgedToken_99af3eea856556646c98c8b9b2548fe815240750.Vault' \ + "0xd069d989e2F44B70c65347d1853C0c67e10a9F8D" \ + '["0xd069d989e2F44B70c65347d1853C0c67e10a9F8D","0x99aF3EeA856556646C98c8B9b2548Fe815240750"]' \ + '[100]' \ + --network mainnet \ + --signer mainnet-admin flow transactions send ./cadence/transactions/flow-yield-vaults/admin/add_strategy_composer.cdc \ 'A.b1d63873c3cc9f79.FlowYieldVaultsStrategiesV2.FUSDEVStrategy' \ From aa4c0f0d1023f90a4a11b3e0779e4edbb7404d6b Mon Sep 17 00:00:00 2001 From: Alex <12097569+nialexsan@users.noreply.github.com> Date: Tue, 24 Feb 2026 11:48:54 -0500 Subject: [PATCH 03/27] add pyusd test --- local/setup_mainnet.sh | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/local/setup_mainnet.sh b/local/setup_mainnet.sh index 261f5cc0..a092a621 100755 --- a/local/setup_mainnet.sh +++ b/local/setup_mainnet.sh @@ -278,6 +278,14 @@ flow transactions send ./lib/FlowALP/cadence/tests/transactions/flow-alp/pool-ma # --network mainnet \ # --signer # +# PYUSD0 +# 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 # # test PEAK MONEY strategy # From 45389a6976bc3784ad636ed8b90a852c2173c218 Mon Sep 17 00:00:00 2001 From: Alex <12097569+nialexsan@users.noreply.github.com> Date: Tue, 24 Feb 2026 12:37:31 -0500 Subject: [PATCH 04/27] Apply suggestion from @nialexsan --- cadence/scripts/band-oracle/get_pyusd_price.cdc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cadence/scripts/band-oracle/get_pyusd_price.cdc b/cadence/scripts/band-oracle/get_pyusd_price.cdc index a454ed37..8c741202 100644 --- a/cadence/scripts/band-oracle/get_pyusd_price.cdc +++ b/cadence/scripts/band-oracle/get_pyusd_price.cdc @@ -28,7 +28,7 @@ fun main(): BandOracle.ReferenceData { // PYUSD is the base symbol; USD is the implicit quote for all Band oracle rates. // The returned fixedPointRate = PYUSD price in USD. let priceData = BandOracle.getReferenceData( - baseSymbol: "USD", + baseSymbol: "PYUSD", quoteSymbol: "USD", payment: <-payment ) From 86d8000c4db584dbc6e68827040e961355333b22 Mon Sep 17 00:00:00 2001 From: Alex <12097569+nialexsan@users.noreply.github.com> Date: Tue, 24 Feb 2026 14:38:35 -0500 Subject: [PATCH 05/27] add syWFLOWv --- .../contracts/FlowYieldVaultsStrategiesV2.cdc | 90 ++++++++++++++++--- 1 file changed, 79 insertions(+), 11 deletions(-) diff --git a/cadence/contracts/FlowYieldVaultsStrategiesV2.cdc b/cadence/contracts/FlowYieldVaultsStrategiesV2.cdc index 05f61355..481132dd 100644 --- a/cadence/contracts/FlowYieldVaultsStrategiesV2.cdc +++ b/cadence/contracts/FlowYieldVaultsStrategiesV2.cdc @@ -72,7 +72,7 @@ access(all) contract FlowYieldVaultsStrategiesV2 { } } - /// This strategy uses FUSDEV vault + /// This strategy uses FUSDEV vault (Morpho ERC4626) access(all) resource FUSDEVStrategy : 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 @@ -132,6 +132,66 @@ access(all) contract FlowYieldVaultsStrategiesV2 { } } + /// This strategy uses syWFLOWv vault (Standard ERC4626) + 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} + + 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.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 { + return ofToken == self.source.getSourceType() ? self.source.minimumAvailable() : 0.0 + } + /// Deposits up to the inner Sink's capacity from the provided authorized Vault reference + access(all) fun deposit(from: auth(FungibleToken.Withdraw) &{FungibleToken.Vault}) { + 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) + } + /// Executed when a Strategy is burned, cleaning up the Strategy's stored AutoBalancer + access(contract) fun burnCallback() { + FlowYieldVaultsAutoBalancers._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 + } + } + access(all) struct TokenBundle { access(all) let moetTokenType: Type access(all) let moetTokenEVMAddress: EVM.EVMAddress @@ -179,8 +239,8 @@ access(all) contract FlowYieldVaultsStrategiesV2 { } } - /// This StrategyComposer builds a Strategy that uses MorphoERC4626 vault - access(all) resource MorphoERC4626StrategyComposer : FlowYieldVaults.StrategyComposer { + /// This StrategyComposer builds a Strategy that uses ERC4626 and MorphoERC4626 vaults + access(all) resource ERC4626StrategyComposer : FlowYieldVaults.StrategyComposer { /// { Strategy Type: { Collateral Type: FlowYieldVaultsStrategiesV2.CollateralConfig } } access(self) let config: {Type: {Type: FlowYieldVaultsStrategiesV2.CollateralConfig}} @@ -311,6 +371,12 @@ access(all) contract FlowYieldVaultsStrategiesV2 { collateralType: collateralType, position: <-position ) + case Type<@syWFLOWvStrategy>(): + return <-create syWFLOWvStrategy( + id: uniqueID, + collateralType: collateralType, + position: <-position + ) default: panic("Unsupported strategy type \(type.identifier)") } @@ -646,12 +712,12 @@ access(all) contract FlowYieldVaultsStrategiesV2 { access(all) view fun getSupportedComposers(): {Type: Bool} { return { - Type<@MorphoERC4626StrategyComposer>(): true + Type<@ERC4626StrategyComposer>(): true } } access(self) view fun isSupportedComposer(_ type: Type): Bool { - return type == Type<@MorphoERC4626StrategyComposer>() + return type == Type<@ERC4626StrategyComposer>() } access(all) fun issueComposer(_ type: Type): @{FlowYieldVaults.StrategyComposer} { pre { @@ -661,8 +727,8 @@ access(all) contract FlowYieldVaultsStrategiesV2 { "Could not find config for StrategyComposer \(type.identifier)" } switch type { - case Type<@MorphoERC4626StrategyComposer>(): - return <- create MorphoERC4626StrategyComposer(self.configs[type]!) + case Type<@ERC4626StrategyComposer>(): + return <- create ERC4626StrategyComposer(self.configs[type]!) default: panic("Unsupported StrategyComposer \(type.identifier) requested") } @@ -741,8 +807,9 @@ access(all) contract FlowYieldVaultsStrategiesV2 { } access(Configure) fun purgeConfig() { self.configs = { - Type<@MorphoERC4626StrategyComposer>(): { - Type<@FUSDEVStrategy>(): {} as {Type: FlowYieldVaultsStrategiesV2.CollateralConfig} + Type<@ERC4626StrategyComposer>(): { + Type<@FUSDEVStrategy>(): {} as {Type: FlowYieldVaultsStrategiesV2.CollateralConfig}, + Type<@syWFLOWvStrategy>(): {} as {Type: FlowYieldVaultsStrategiesV2.CollateralConfig} } } } @@ -827,8 +894,9 @@ access(all) contract FlowYieldVaultsStrategiesV2 { } let configs = { - Type<@MorphoERC4626StrategyComposer>(): { - Type<@FUSDEVStrategy>(): {} as {Type: FlowYieldVaultsStrategiesV2.CollateralConfig} + Type<@ERC4626StrategyComposer>(): { + Type<@FUSDEVStrategy>(): {} as {Type: FlowYieldVaultsStrategiesV2.CollateralConfig}, + Type<@syWFLOWvStrategy>(): {} as {Type: FlowYieldVaultsStrategiesV2.CollateralConfig} } } self.account.storage.save(<-create StrategyComposerIssuer(configs: configs), to: self.IssuerStoragePath) From 251ca86d37d4cf77ace4aeb1364383528089bc29 Mon Sep 17 00:00:00 2001 From: Alex <12097569+nialexsan@users.noreply.github.com> Date: Wed, 25 Feb 2026 23:06:02 -0500 Subject: [PATCH 06/27] revert renaming --- cadence/contracts/FlowYieldVaultsStrategiesV2.cdc | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/cadence/contracts/FlowYieldVaultsStrategiesV2.cdc b/cadence/contracts/FlowYieldVaultsStrategiesV2.cdc index 481132dd..f763672a 100644 --- a/cadence/contracts/FlowYieldVaultsStrategiesV2.cdc +++ b/cadence/contracts/FlowYieldVaultsStrategiesV2.cdc @@ -240,7 +240,7 @@ access(all) contract FlowYieldVaultsStrategiesV2 { } /// This StrategyComposer builds a Strategy that uses ERC4626 and MorphoERC4626 vaults - access(all) resource ERC4626StrategyComposer : FlowYieldVaults.StrategyComposer { + access(all) resource MorphoERC4626StrategyComposer : FlowYieldVaults.StrategyComposer { /// { Strategy Type: { Collateral Type: FlowYieldVaultsStrategiesV2.CollateralConfig } } access(self) let config: {Type: {Type: FlowYieldVaultsStrategiesV2.CollateralConfig}} @@ -712,12 +712,12 @@ access(all) contract FlowYieldVaultsStrategiesV2 { access(all) view fun getSupportedComposers(): {Type: Bool} { return { - Type<@ERC4626StrategyComposer>(): true + Type<@MorphoERC4626StrategyComposer>(): true } } access(self) view fun isSupportedComposer(_ type: Type): Bool { - return type == Type<@ERC4626StrategyComposer>() + return type == Type<@MorphoERC4626StrategyComposer>() } access(all) fun issueComposer(_ type: Type): @{FlowYieldVaults.StrategyComposer} { pre { @@ -727,8 +727,8 @@ access(all) contract FlowYieldVaultsStrategiesV2 { "Could not find config for StrategyComposer \(type.identifier)" } switch type { - case Type<@ERC4626StrategyComposer>(): - return <- create ERC4626StrategyComposer(self.configs[type]!) + case Type<@MorphoERC4626StrategyComposer>(): + return <- create MorphoERC4626StrategyComposer(self.configs[type]!) default: panic("Unsupported StrategyComposer \(type.identifier) requested") } @@ -807,7 +807,7 @@ access(all) contract FlowYieldVaultsStrategiesV2 { } access(Configure) fun purgeConfig() { self.configs = { - Type<@ERC4626StrategyComposer>(): { + Type<@MorphoERC4626StrategyComposer>(): { Type<@FUSDEVStrategy>(): {} as {Type: FlowYieldVaultsStrategiesV2.CollateralConfig}, Type<@syWFLOWvStrategy>(): {} as {Type: FlowYieldVaultsStrategiesV2.CollateralConfig} } @@ -894,7 +894,7 @@ access(all) contract FlowYieldVaultsStrategiesV2 { } let configs = { - Type<@ERC4626StrategyComposer>(): { + Type<@MorphoERC4626StrategyComposer>(): { Type<@FUSDEVStrategy>(): {} as {Type: FlowYieldVaultsStrategiesV2.CollateralConfig}, Type<@syWFLOWvStrategy>(): {} as {Type: FlowYieldVaultsStrategiesV2.CollateralConfig} } From fa48ed708c137a108fec863df5a31b5c0892a0f7 Mon Sep 17 00:00:00 2001 From: Alex <12097569+nialexsan@users.noreply.github.com> Date: Fri, 27 Feb 2026 23:19:36 -0500 Subject: [PATCH 07/27] WIP strategy --- .../contracts/FlowYieldVaultsStrategiesV2.cdc | 102 ++++++++++-------- 1 file changed, 59 insertions(+), 43 deletions(-) diff --git a/cadence/contracts/FlowYieldVaultsStrategiesV2.cdc b/cadence/contracts/FlowYieldVaultsStrategiesV2.cdc index f763672a..e327d5be 100644 --- a/cadence/contracts/FlowYieldVaultsStrategiesV2.cdc +++ b/cadence/contracts/FlowYieldVaultsStrategiesV2.cdc @@ -193,8 +193,10 @@ access(all) contract FlowYieldVaultsStrategiesV2 { } access(all) struct TokenBundle { - access(all) let moetTokenType: Type - access(all) let moetTokenEVMAddress: EVM.EVMAddress + /// The debt token type (what gets borrowed from the pool) + /// This is the pool's defaultToken - could be MOET, USDC, or any other token + access(all) let debtTokenType: Type + access(all) let debtTokenEVMAddress: EVM.EVMAddress access(all) let yieldTokenType: Type access(all) let yieldTokenEVMAddress: EVM.EVMAddress @@ -203,15 +205,15 @@ access(all) contract FlowYieldVaultsStrategiesV2 { access(all) let underlying4626AssetEVMAddress: EVM.EVMAddress init( - moetTokenType: Type, - moetTokenEVMAddress: EVM.EVMAddress, + debtTokenType: Type, + debtTokenEVMAddress: EVM.EVMAddress, yieldTokenType: Type, yieldTokenEVMAddress: EVM.EVMAddress, underlying4626AssetType: Type, underlying4626AssetEVMAddress: EVM.EVMAddress ) { - self.moetTokenType = moetTokenType - self.moetTokenEVMAddress = moetTokenEVMAddress + self.debtTokenType = debtTokenType + self.debtTokenEVMAddress = debtTokenEVMAddress self.yieldTokenType = yieldTokenType self.yieldTokenEVMAddress = yieldTokenEVMAddress self.underlying4626AssetType = underlying4626AssetType @@ -319,19 +321,21 @@ access(all) contract FlowYieldVaultsStrategiesV2 { uniqueID: uniqueID ) - // Swappers: MOET <-> YIELD (YIELD is ERC4626 vault token) - let moetToYieldSwapper = self._createMoetToYieldSwapper(strategyType: type, tokens: tokens, uniqueID: uniqueID) + // Swappers: DEBT <-> YIELD + // DEBT is the pool's borrowable token (e.g., MOET, USDC) + // YIELD is the ERC4626 vault token + let debtToYieldSwapper = self._createDebtToYieldSwapper(strategyType: type, tokens: tokens, uniqueID: uniqueID) - let yieldToMoetSwapper = self._createYieldToMoetSwapper(strategyType: type, tokens: tokens, uniqueID: uniqueID) + let yieldToDebtSwapper = self._createYieldToDebtSwapper(strategyType: type, tokens: tokens, uniqueID: uniqueID) // AutoBalancer-directed swap IO let abaSwapSink = SwapConnectors.SwapSink( - swapper: moetToYieldSwapper, + swapper: debtToYieldSwapper, sink: balancerIO.sink, uniqueID: uniqueID ) let abaSwapSource = SwapConnectors.SwapSource( - swapper: yieldToMoetSwapper, + swapper: yieldToDebtSwapper, source: balancerIO.source, uniqueID: uniqueID ) @@ -401,13 +405,25 @@ access(all) contract FlowYieldVaultsStrategiesV2 { ) } + /// Gets the Pool's default token type (the borrowable token) + access(self) fun _getPoolDefaultToken(): Type { + let poolCap = FlowYieldVaultsStrategiesV2.account.storage.copy< + Capability + >(from: FlowALPv0.PoolCapStoragePath) + ?? panic("Missing or invalid pool capability") + + let poolRef = poolCap.borrow() ?? panic("Invalid Pool Cap") + return poolRef.getDefaultToken() + } + access(self) fun _resolveTokenBundle( collateralConfig: FlowYieldVaultsStrategiesV2.CollateralConfig ): FlowYieldVaultsStrategiesV2.TokenBundle { - // MOET - let moetTokenType = Type<@MOET.Vault>() - let moetTokenEVMAddress = FlowEVMBridgeConfig.getEVMAddressAssociated(with: moetTokenType) - ?? panic("Token Vault type \(moetTokenType.identifier) has not yet been registered with the VMbridge") + // Get the Pool's default token (the borrowable debt token) + // This could be MOET, USDC, or any other token the pool is configured to lend + let debtTokenType = self._getPoolDefaultToken() + let debtTokenEVMAddress = FlowEVMBridgeConfig.getEVMAddressAssociated(with: debtTokenType) + ?? panic("Token Vault type \(debtTokenType.identifier) has not yet been registered with the VMbridge") // YIELD (ERC4626 vault token) let yieldTokenEVMAddress = collateralConfig.yieldTokenEVMAddress @@ -427,8 +443,8 @@ access(all) contract FlowYieldVaultsStrategiesV2 { ) return FlowYieldVaultsStrategiesV2.TokenBundle( - moetTokenType: moetTokenType, - moetTokenEVMAddress: moetTokenEVMAddress, + debtTokenType: debtTokenType, + debtTokenEVMAddress: debtTokenEVMAddress, yieldTokenType: yieldTokenType, yieldTokenEVMAddress: yieldTokenEVMAddress, underlying4626AssetType: underlying4626AssetType, @@ -468,25 +484,25 @@ access(all) contract FlowYieldVaultsStrategiesV2 { ) } - access(self) fun _createMoetToYieldSwapper( + access(self) fun _createDebtToYieldSwapper( strategyType: Type, tokens: FlowYieldVaultsStrategiesV2.TokenBundle, uniqueID: DeFiActions.UniqueIdentifier ): SwapConnectors.MultiSwapper { - // Direct MOET -> YIELD via AMM - let moetToYieldAMM = self._createUniV3Swapper( - tokenPath: [tokens.moetTokenEVMAddress, tokens.yieldTokenEVMAddress], + // Direct DEBT -> YIELD via AMM + let debtToYieldAMM = self._createUniV3Swapper( + tokenPath: [tokens.debtTokenEVMAddress, tokens.yieldTokenEVMAddress], feePath: [100], - inVault: tokens.moetTokenType, + inVault: tokens.debtTokenType, outVault: tokens.yieldTokenType, uniqueID: uniqueID ) - // MOET -> UNDERLYING via AMM - let moetToUnderlying = self._createUniV3Swapper( - tokenPath: [tokens.moetTokenEVMAddress, tokens.underlying4626AssetEVMAddress], + // DEBT -> UNDERLYING via AMM + let debtToUnderlying = self._createUniV3Swapper( + tokenPath: [tokens.debtTokenEVMAddress, tokens.underlying4626AssetEVMAddress], feePath: [100], - inVault: tokens.moetTokenType, + inVault: tokens.debtTokenType, outVault: tokens.underlying4626AssetType, uniqueID: uniqueID ) @@ -513,29 +529,29 @@ access(all) contract FlowYieldVaultsStrategiesV2 { } let seq = SwapConnectors.SequentialSwapper( - swappers: [moetToUnderlying, underlyingTo4626!], + swappers: [debtToUnderlying, underlyingTo4626!], uniqueID: uniqueID ) return SwapConnectors.MultiSwapper( - inVault: tokens.moetTokenType, + inVault: tokens.debtTokenType, outVault: tokens.yieldTokenType, - swappers: [moetToYieldAMM, seq], + swappers: [debtToYieldAMM, seq], uniqueID: uniqueID ) } - access(self) fun _createYieldToMoetSwapper( + access(self) fun _createYieldToDebtSwapper( strategyType: Type, tokens: FlowYieldVaultsStrategiesV2.TokenBundle, uniqueID: DeFiActions.UniqueIdentifier ): SwapConnectors.MultiSwapper { - // Direct YIELD -> MOET via AMM - let yieldToMoetAMM = self._createUniV3Swapper( - tokenPath: [tokens.yieldTokenEVMAddress, tokens.moetTokenEVMAddress], + // Direct YIELD -> DEBT via AMM + let yieldToDebtAMM = self._createUniV3Swapper( + tokenPath: [tokens.yieldTokenEVMAddress, tokens.debtTokenEVMAddress], feePath: [100], inVault: tokens.yieldTokenType, - outVault: tokens.moetTokenType, + outVault: tokens.debtTokenType, uniqueID: uniqueID ) @@ -549,32 +565,32 @@ access(all) contract FlowYieldVaultsStrategiesV2 { uniqueID: uniqueID, isReversed: true ) - // UNDERLYING -> MOET via AMM - let underlyingToMoet = self._createUniV3Swapper( - tokenPath: [tokens.underlying4626AssetEVMAddress, tokens.moetTokenEVMAddress], + // UNDERLYING -> DEBT via AMM + let underlyingToDebt = self._createUniV3Swapper( + tokenPath: [tokens.underlying4626AssetEVMAddress, tokens.debtTokenEVMAddress], feePath: [100], inVault: tokens.underlying4626AssetType, - outVault: tokens.moetTokenType, + outVault: tokens.debtTokenType, uniqueID: uniqueID ) let seq = SwapConnectors.SequentialSwapper( - swappers: [yieldToUnderlying, underlyingToMoet], + swappers: [yieldToUnderlying, underlyingToDebt], uniqueID: uniqueID ) return SwapConnectors.MultiSwapper( inVault: tokens.yieldTokenType, - outVault: tokens.moetTokenType, - swappers: [yieldToMoetAMM, seq], + outVault: tokens.debtTokenType, + swappers: [yieldToDebtAMM, seq], uniqueID: uniqueID ) } else { // Standard ERC4626: AMM-only reverse (no synchronous redeem support) return SwapConnectors.MultiSwapper( inVault: tokens.yieldTokenType, - outVault: tokens.moetTokenType, - swappers: [yieldToMoetAMM], + outVault: tokens.debtTokenType, + swappers: [yieldToDebtAMM], uniqueID: uniqueID ) } From 5f33347e0a6ad786ec4d9ab1cd6a97396f5979b7 Mon Sep 17 00:00:00 2001 From: Alex <12097569+nialexsan@users.noreply.github.com> Date: Mon, 9 Mar 2026 00:15:50 -0400 Subject: [PATCH 08/27] generate forked tests --- .../contracts/FlowYieldVaultsStrategiesV2.cdc | 1224 ++++++++++++----- ...wYieldVaultsStrategiesV2_syWFLOWv_test.cdc | 515 +++++++ .../tests/PMStrategiesV1_syWFLOWv_test.cdc | 15 +- .../transactions/provision_wbtc_from_weth.cdc | 96 ++ .../admin/recreate_composer_issuer.cdc | 23 + .../admin/upsert_more_erc4626_config.cdc | 56 + flow.json | 2 +- 7 files changed, 1568 insertions(+), 363 deletions(-) create mode 100644 cadence/tests/FlowYieldVaultsStrategiesV2_syWFLOWv_test.cdc create mode 100644 cadence/tests/transactions/provision_wbtc_from_weth.cdc create mode 100644 cadence/transactions/flow-yield-vaults/admin/recreate_composer_issuer.cdc create mode 100644 cadence/transactions/flow-yield-vaults/admin/upsert_more_erc4626_config.cdc diff --git a/cadence/contracts/FlowYieldVaultsStrategiesV2.cdc b/cadence/contracts/FlowYieldVaultsStrategiesV2.cdc index 00d89151..a29ab62c 100644 --- a/cadence/contracts/FlowYieldVaultsStrategiesV2.cdc +++ b/cadence/contracts/FlowYieldVaultsStrategiesV2.cdc @@ -1,4 +1,5 @@ // standards +import "Burner" import "FungibleToken" import "EVM" // DeFiActions @@ -42,6 +43,11 @@ access(all) contract FlowYieldVaultsStrategiesV2 { access(all) let univ3RouterEVMAddress: EVM.EVMAddress access(all) let univ3QuoterEVMAddress: EVM.EVMAddress + /// Partitioned config map. Each key is a partition name; each value is a typed nested map keyed by + /// strategy UniqueIdentifier ID (UInt64). Current partitions: + /// "yieldToMoetSwappers" → {UInt64: {DeFiActions.Swapper}} + /// "debtToCollateralSwappers" → {UInt64: {DeFiActions.Swapper}} + /// "closedPositions" → {UInt64: Bool} access(contract) let config: {String: AnyStruct} /// Canonical StoragePath where the StrategyComposerIssuer should be stored @@ -72,6 +78,37 @@ access(all) contract FlowYieldVaultsStrategiesV2 { } } + /// Collateral configuration for strategies that borrow the vault's underlying asset directly, + /// using a standard ERC4626 deposit for the forward path (underlying → yield token) and a + /// UniV3 AMM swap for the reverse path (yield token → underlying). This applies to "More" + /// ERC4626 vaults that do not support synchronous redemptions via ERC4626 redeem(). + access(all) struct MoreERC4626CollateralConfig { + access(all) let yieldTokenEVMAddress: EVM.EVMAddress + /// UniV3 path for swapping yield token → underlying asset (used for debt repayment and + /// AutoBalancer rebalancing). The path must start with the yield token EVM address. + access(all) let yieldToUnderlyingUniV3AddressPath: [EVM.EVMAddress] + access(all) let yieldToUnderlyingUniV3FeePath: [UInt32] + /// UniV3 path for swapping debt token → collateral (used to convert overpayment dust + /// returned by position.closePosition back into collateral). The path must start with + /// the debt token EVM address and end with the collateral EVM address. + access(all) let debtToCollateralUniV3AddressPath: [EVM.EVMAddress] + access(all) let debtToCollateralUniV3FeePath: [UInt32] + + init( + yieldTokenEVMAddress: EVM.EVMAddress, + yieldToUnderlyingUniV3AddressPath: [EVM.EVMAddress], + yieldToUnderlyingUniV3FeePath: [UInt32], + debtToCollateralUniV3AddressPath: [EVM.EVMAddress], + debtToCollateralUniV3FeePath: [UInt32] + ) { + self.yieldTokenEVMAddress = yieldTokenEVMAddress + self.yieldToUnderlyingUniV3AddressPath = yieldToUnderlyingUniV3AddressPath + self.yieldToUnderlyingUniV3FeePath = yieldToUnderlyingUniV3FeePath + self.debtToCollateralUniV3AddressPath = debtToCollateralUniV3AddressPath + self.debtToCollateralUniV3FeePath = debtToCollateralUniV3FeePath + } + } + /// This strategy uses FUSDEV vault (Morpho ERC4626). /// Deposits collateral into a single FlowALP position, borrowing MOET as debt. /// MOET is swapped to PYUSD0 and deposited into the Morpho FUSDEV ERC4626 vault. @@ -84,10 +121,6 @@ access(all) contract FlowYieldVaultsStrategiesV2 { access(self) let position: @FlowALPv0.Position access(self) var sink: {DeFiActions.Sink} access(self) var source: {DeFiActions.Source} - /// Tracks whether the underlying FlowALP position has been closed. Once true, - /// availableBalance() returns 0.0 to avoid panicking when the pool no longer - /// holds the position (e.g. during YieldVault burnCallback after close). - access(self) var positionClosed: Bool init( id: DeFiActions.UniqueIdentifier, @@ -97,7 +130,6 @@ access(all) contract FlowYieldVaultsStrategiesV2 { self.uniqueID = id self.sink = position.createSink(type: collateralType) self.source = position.createSourceWithOptions(type: collateralType, pullFromTopUpSource: true) - self.positionClosed = false self.position <-position } @@ -109,7 +141,7 @@ access(all) contract FlowYieldVaultsStrategiesV2 { } /// Returns the amount available for withdrawal via the inner Source access(all) fun availableBalance(ofToken: Type): UFix64 { - if self.positionClosed { return 0.0 } + if FlowYieldVaultsStrategiesV2._isPositionClosed(self.uniqueID) { 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. @@ -179,12 +211,12 @@ access(all) contract FlowYieldVaultsStrategiesV2 { // Zero vaults: dust collateral rounded down to zero — return an empty vault if resultVaults.length == 0 { destroy resultVaults - self.positionClosed = true + FlowYieldVaultsStrategiesV2._markPositionClosed(self.uniqueID) return <- DeFiActionsUtils.getEmptyVault(collateralType) } let collateralVault <- resultVaults.removeFirst() destroy resultVaults - self.positionClosed = true + FlowYieldVaultsStrategiesV2._markPositionClosed(self.uniqueID) return <- collateralVault } @@ -192,9 +224,8 @@ access(all) contract FlowYieldVaultsStrategiesV2 { let yieldTokenSource = FlowYieldVaultsAutoBalancers.createExternalSource(id: self.id()!) ?? panic("Could not create external source from AutoBalancer") - // Step 5: Retrieve yield→MOET swapper from contract config - let swapperKey = FlowYieldVaultsStrategiesV2.getYieldToMoetSwapperConfigKey(self.uniqueID)! - let yieldToMoetSwapper = FlowYieldVaultsStrategiesV2.config[swapperKey] as! {DeFiActions.Swapper}? + // Step 5: Retrieve yield→MOET swapper + let yieldToMoetSwapper = FlowYieldVaultsStrategiesV2._getYieldToMoetSwapper(self.uniqueID!.id) ?? panic("No yield→MOET swapper found for strategy \(self.id()!)") // Step 6: Create a SwapSource that converts yield tokens to MOET when pulled by closePosition. @@ -222,28 +253,40 @@ access(all) contract FlowYieldVaultsStrategiesV2 { message: "First vault returned from closePosition must be collateral (\(collateralType.identifier)), got \(collateralVault.getType().identifier)" ) - // Handle any overpayment dust (MOET) returned as the second vault + // Handle any overpayment dust (MOET) returned as the second vault. + // nil means no swapper configured (old positions) — dust will be destroyed. + let debtToCollateralSwapper = FlowYieldVaultsStrategiesV2._getDebtToCollateralSwapper(self.uniqueID!.id) + while resultVaults.length > 0 { let dustVault <- resultVaults.removeFirst() if dustVault.balance > 0.0 { if dustVault.getType() == collateralType { collateralVault.deposit(from: <-dustVault) + } else if let swapper = debtToCollateralSwapper { + // Quote first — if dust is too small to route, destroy it + let quote = swapper.quoteOut(forProvided: dustVault.balance, reverse: false) + if quote.outAmount > 0.0 { + let swapped <- swapper.swap(quote: quote, inVault: <-dustVault) + collateralVault.deposit(from: <-swapped) + } else { + Burner.burn(<-dustVault) + } } else { - // @TODO implement swapping moet to collateral - destroy dustVault + Burner.burn(<-dustVault) } } else { - destroy dustVault + Burner.burn(<-dustVault) } } destroy resultVaults - self.positionClosed = true + FlowYieldVaultsStrategiesV2._markPositionClosed(self.uniqueID) return <- collateralVault } /// Executed when a Strategy is burned, cleaning up the Strategy's stored AutoBalancer access(contract) fun burnCallback() { FlowYieldVaultsAutoBalancers._cleanupAutoBalancer(id: self.id()!) + FlowYieldVaultsStrategiesV2._cleanupPositionClosed(self.uniqueID) } access(all) fun getComponentInfo(): DeFiActions.ComponentInfo { return DeFiActions.ComponentInfo( @@ -263,7 +306,11 @@ access(all) contract FlowYieldVaultsStrategiesV2 { } } - /// This strategy uses syWFLOWv vault (Standard ERC4626) + /// 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 @@ -271,11 +318,29 @@ access(all) contract FlowYieldVaultsStrategiesV2 { access(self) let position: @FlowALPv0.Position access(self) var sink: {DeFiActions.Sink} access(self) var source: {DeFiActions.Source} + // debtTokenType moved to contract-level config["syWFLOWvDebtTokenTypes"] keyed by uniqueID.id + /// Swapper used in closePosition to source FLOW from the AutoBalancer (syWFLOWv → FLOW via UniV3). + access(self) let yieldToDebtSwapper: {DeFiActions.Swapper} + /// Swapper used in closePosition to convert FLOW overpayment dust back to collateral (FLOW → collateral via UniV3). + access(self) let debtToCollateralSwapper: {DeFiActions.Swapper} + /// Tracks whether the underlying FlowALP position has been closed. Once true, + /// availableBalance() returns 0.0 to avoid panicking when the pool no longer + /// holds the position (e.g. during YieldVault burnCallback after close). + access(self) var positionClosed: Bool - init(id: DeFiActions.UniqueIdentifier, collateralType: Type, position: @FlowALPv0.Position) { + init( + id: DeFiActions.UniqueIdentifier, + collateralType: Type, + position: @FlowALPv0.Position, + yieldToDebtSwapper: {DeFiActions.Swapper}, + debtToCollateralSwapper: {DeFiActions.Swapper} + ) { self.uniqueID = id self.sink = position.createSink(type: collateralType) self.source = position.createSourceWithOptions(type: collateralType, pullFromTopUpSource: true) + self.yieldToDebtSwapper = yieldToDebtSwapper + self.debtToCollateralSwapper = debtToCollateralSwapper + self.positionClosed = false self.position <-position } @@ -287,10 +352,24 @@ access(all) contract FlowYieldVaultsStrategiesV2 { } /// 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 + /// 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)" + } + // Reject the debt token (FLOW) as collateral — looked up from contract-level config + if let id = self.uniqueID { + let debtTokenType = FlowYieldVaultsStrategiesV2._getSyWFLOWvDebtTokenType(id.id) + assert( + debtTokenType == nil || from.getType() != debtTokenType!, + message: "syWFLOWvStrategy: FLOW cannot be used as collateral — it is the vault's underlying asset" + ) + } self.sink.depositCapacity(from: from) } /// Withdraws up to the max amount, returning the withdrawn Vault. If the requested token type is unsupported, @@ -301,9 +380,102 @@ access(all) contract FlowYieldVaultsStrategiesV2 { } return <- self.source.withdrawAvailable(maxAmount: maxAmount) } - /// Executed when a Strategy is burned, cleaning up the Strategy's stored AutoBalancer + /// 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) + } + let collateralVault <- resultVaults.removeFirst() + destroy resultVaults + self.positionClosed = true + return <- collateralVault + } + + // Step 3: Create external syWFLOWv source from AutoBalancer + let yieldTokenSource = FlowYieldVaultsAutoBalancers.createExternalSource(id: self.id()!) + ?? panic("Could not create external source from AutoBalancer") + + // Step 4: Create a SwapSource that converts syWFLOWv → FLOW for debt repayment + let flowSource = SwapConnectors.SwapSource( + swapper: self.yieldToDebtSwapper, + source: yieldTokenSource, + uniqueID: self.copyID() + ) + + // Step 5: Close position — pool pulls exactly the FLOW debt amount from flowSource + let resultVaults <- self.position.closePosition(repaymentSources: [flowSource]) + + assert( + resultVaults.length >= 1 && resultVaults.length <= 2, + message: "Expected 1 or 2 vaults from closePosition, got \(resultVaults.length)" + ) + + var collateralVault <- resultVaults.removeFirst() + assert( + collateralVault.getType() == collateralType, + message: "First vault returned from closePosition must be collateral (\(collateralType.identifier)), got \(collateralVault.getType().identifier)" + ) + + // Handle any overpayment dust (FLOW) returned as the second vault. + while resultVaults.length > 0 { + let dustVault <- resultVaults.removeFirst() + if dustVault.balance > 0.0 { + if dustVault.getType() == collateralType { + collateralVault.deposit(from: <-dustVault) + } else { + // Quote first — if dust is too small to route, destroy it + let quote = self.debtToCollateralSwapper.quoteOut(forProvided: dustVault.balance, reverse: false) + if quote.outAmount > 0.0 { + let swapped <- self.debtToCollateralSwapper.swap(quote: quote, inVault: <-dustVault) + collateralVault.deposit(from: <-swapped) + } else { + Burner.burn(<-dustVault) + } + } + } else { + Burner.burn(<-dustVault) + } + } + + destroy resultVaults + self.positionClosed = true + return <- collateralVault + } + /// Executed when a Strategy is burned, cleaning up the Strategy's stored AutoBalancer and contract-level config entries access(contract) fun burnCallback() { FlowYieldVaultsAutoBalancers._cleanupAutoBalancer(id: self.id()!) + FlowYieldVaultsStrategiesV2._removeSyWFLOWvDebtTokenType(self.uniqueID?.id) } access(all) fun getComponentInfo(): DeFiActions.ComponentInfo { return DeFiActions.ComponentInfo( @@ -324,10 +496,9 @@ access(all) contract FlowYieldVaultsStrategiesV2 { } access(all) struct TokenBundle { - /// The debt token type (what gets borrowed from the pool) - /// This is the pool's defaultToken - could be MOET, USDC, or any other token - access(all) let debtTokenType: Type - access(all) let debtTokenEVMAddress: EVM.EVMAddress + /// The MOET token type (the pool's borrowable token) + access(all) let moetTokenType: Type + access(all) let moetTokenEVMAddress: EVM.EVMAddress access(all) let yieldTokenType: Type access(all) let yieldTokenEVMAddress: EVM.EVMAddress @@ -336,15 +507,15 @@ access(all) contract FlowYieldVaultsStrategiesV2 { access(all) let underlying4626AssetEVMAddress: EVM.EVMAddress init( - debtTokenType: Type, - debtTokenEVMAddress: EVM.EVMAddress, + moetTokenType: Type, + moetTokenEVMAddress: EVM.EVMAddress, yieldTokenType: Type, yieldTokenEVMAddress: EVM.EVMAddress, underlying4626AssetType: Type, underlying4626AssetEVMAddress: EVM.EVMAddress ) { - self.debtTokenType = debtTokenType - self.debtTokenEVMAddress = debtTokenEVMAddress + self.moetTokenType = moetTokenType + self.moetTokenEVMAddress = moetTokenEVMAddress self.yieldTokenType = yieldTokenType self.yieldTokenEVMAddress = yieldTokenEVMAddress self.underlying4626AssetType = underlying4626AssetType @@ -372,12 +543,116 @@ access(all) contract FlowYieldVaultsStrategiesV2 { } } - /// This StrategyComposer builds a Strategy that uses ERC4626 and MorphoERC4626 vaults + /* =========================== + Contract-level shared infrastructure + =========================== */ + + /// Gets the Pool's default token type (the borrowable token) + access(self) fun _getPoolDefaultToken(): Type { + let poolCap = FlowYieldVaultsStrategiesV2.account.storage.copy< + Capability + >(from: FlowALPv0.PoolCapStoragePath) + ?? panic("Missing or invalid pool capability") + let poolRef = poolCap.borrow() ?? panic("Invalid Pool Cap") + return poolRef.getDefaultToken() + } + + /// Resolves the full token bundle for a strategy given the ERC4626 yield vault address. + /// The MOET token is always the pool's default token. + access(self) fun _resolveTokenBundle(yieldTokenEVMAddress: EVM.EVMAddress): FlowYieldVaultsStrategiesV2.TokenBundle { + let moetTokenType = FlowYieldVaultsStrategiesV2._getPoolDefaultToken() + let moetTokenEVMAddress = FlowEVMBridgeConfig.getEVMAddressAssociated(with: moetTokenType) + ?? panic("Token Vault type \(moetTokenType.identifier) has not yet been registered with the VMbridge") + + let yieldTokenType = FlowEVMBridgeConfig.getTypeAssociated(with: yieldTokenEVMAddress) + ?? panic("Could not retrieve the VM Bridge associated Type for the yield token address \(yieldTokenEVMAddress.toString())") + + let underlying4626AssetEVMAddress = ERC4626Utils.underlyingAssetEVMAddress(vault: yieldTokenEVMAddress) + ?? panic("Could not get the underlying asset's EVM address for ERC4626Vault \(yieldTokenEVMAddress.toString())") + let underlying4626AssetType = FlowEVMBridgeConfig.getTypeAssociated(with: underlying4626AssetEVMAddress) + ?? panic("Could not retrieve the VM Bridge associated Type for the ERC4626 underlying asset \(underlying4626AssetEVMAddress.toString())") + + return FlowYieldVaultsStrategiesV2.TokenBundle( + moetTokenType: moetTokenType, + moetTokenEVMAddress: moetTokenEVMAddress, + yieldTokenType: yieldTokenType, + yieldTokenEVMAddress: yieldTokenEVMAddress, + underlying4626AssetType: underlying4626AssetType, + underlying4626AssetEVMAddress: underlying4626AssetEVMAddress + ) + } + + access(self) fun _createYieldTokenOracle( + yieldTokenEVMAddress: EVM.EVMAddress, + underlyingAssetType: Type, + uniqueID: DeFiActions.UniqueIdentifier + ): ERC4626PriceOracles.PriceOracle { + return ERC4626PriceOracles.PriceOracle( + vault: yieldTokenEVMAddress, + asset: underlyingAssetType, + uniqueID: uniqueID + ) + } + + access(self) fun _initAutoBalancerAndIO( + oracle: {DeFiActions.PriceOracle}, + yieldTokenType: Type, + recurringConfig: DeFiActions.AutoBalancerRecurringConfig?, + uniqueID: DeFiActions.UniqueIdentifier + ): FlowYieldVaultsStrategiesV2.AutoBalancerIO { + let autoBalancerRef = + FlowYieldVaultsAutoBalancers._initNewAutoBalancer( + oracle: oracle, + vaultType: yieldTokenType, + lowerThreshold: 0.95, + upperThreshold: 1.05, + rebalanceSink: nil, + rebalanceSource: nil, + recurringConfig: recurringConfig, + uniqueID: uniqueID + ) + + let sink = autoBalancerRef.createBalancerSink() + ?? panic("Could not retrieve Sink from AutoBalancer with id \(uniqueID.id)") + let source = autoBalancerRef.createBalancerSource() + ?? panic("Could not retrieve Source from AutoBalancer with id \(uniqueID.id)") + + return FlowYieldVaultsStrategiesV2.AutoBalancerIO( + autoBalancer: autoBalancerRef, + sink: sink, + source: source + ) + } + + access(self) fun _openCreditPosition( + funds: @{FungibleToken.Vault}, + issuanceSink: {DeFiActions.Sink}, + repaymentSource: {DeFiActions.Source} + ): @FlowALPv0.Position { + let poolCap = FlowYieldVaultsStrategiesV2.account.storage.copy< + Capability + >(from: FlowALPv0.PoolCapStoragePath) + ?? panic("Missing or invalid pool capability") + + let poolRef = poolCap.borrow() ?? panic("Invalid Pool Cap") + + let position <- poolRef.createPosition( + funds: <-funds, + issuanceSink: issuanceSink, + repaymentSource: repaymentSource, + pushToDrawDownSink: true + ) + + return <-position + } + + /// This StrategyComposer builds a Strategy that uses ERC4626 and MorphoERC4626 vaults. + /// Only handles FUSDEVStrategy (Morpho-based strategies that require UniV3 swap paths). access(all) resource MorphoERC4626StrategyComposer : FlowYieldVaults.StrategyComposer { - /// { Strategy Type: { Collateral Type: FlowYieldVaultsStrategiesV2.CollateralConfig } } - access(self) let config: {Type: {Type: FlowYieldVaultsStrategiesV2.CollateralConfig}} + /// { Strategy Type: { Collateral Type: CollateralConfig } } + access(self) let config: {Type: {Type: CollateralConfig}} - init(_ config: {Type: {Type: FlowYieldVaultsStrategiesV2.CollateralConfig}}) { + init(_ config: {Type: {Type: CollateralConfig}}) { self.config = config } @@ -393,7 +668,7 @@ access(all) contract FlowYieldVaultsStrategiesV2 { /// Returns the Vault types which can be used to initialize a given Strategy access(all) view fun getSupportedInitializationVaults(forStrategy: Type): {Type: Bool} { let supported: {Type: Bool} = {} - if let strategyConfig = &self.config[forStrategy] as &{Type: FlowYieldVaultsStrategiesV2.CollateralConfig}? { + if let strategyConfig = self.config[forStrategy] { for collateralType in strategyConfig.keys { supported[collateralType] = true } @@ -432,10 +707,12 @@ access(all) contract FlowYieldVaultsStrategiesV2 { collateralType: collateralType ) - let tokens = self._resolveTokenBundle(collateralConfig: collateralConfig) + let tokens = FlowYieldVaultsStrategiesV2._resolveTokenBundle( + yieldTokenEVMAddress: collateralConfig.yieldTokenEVMAddress + ) // Oracle used by AutoBalancer (tracks NAV of ERC4626 vault) - let yieldTokenOracle = self._createYieldTokenOracle( + let yieldTokenOracle = FlowYieldVaultsStrategiesV2._createYieldTokenOracle( yieldTokenEVMAddress: tokens.yieldTokenEVMAddress, underlyingAssetType: tokens.underlying4626AssetType, uniqueID: uniqueID @@ -445,110 +722,103 @@ access(all) contract FlowYieldVaultsStrategiesV2 { let recurringConfig = FlowYieldVaultsStrategiesV2._createRecurringConfig(withID: uniqueID) // Create/store/publish/register AutoBalancer (returns authorized ref) - let balancerIO = self._initAutoBalancerAndIO( + let balancerIO = FlowYieldVaultsStrategiesV2._initAutoBalancerAndIO( oracle: yieldTokenOracle, yieldTokenType: tokens.yieldTokenType, recurringConfig: recurringConfig, uniqueID: uniqueID ) - // Swappers: DEBT <-> YIELD - // DEBT is the pool's borrowable token (e.g., MOET, USDC) - // YIELD is the ERC4626 vault token - let debtToYieldSwapper = self._createDebtToYieldSwapper(strategyType: type, tokens: tokens, uniqueID: uniqueID) - - let yieldToDebtSwapper = self._createYieldToDebtSwapper(strategyType: type, tokens: tokens, uniqueID: uniqueID) + switch type { - // AutoBalancer-directed swap IO - let abaSwapSink = SwapConnectors.SwapSink( - swapper: debtToYieldSwapper, - sink: balancerIO.sink, - uniqueID: uniqueID - ) - let abaSwapSource = SwapConnectors.SwapSource( - swapper: yieldToDebtSwapper, - source: balancerIO.source, - uniqueID: uniqueID - ) + // ----------------------------------------------------------------------- + // FUSDEVStrategy: borrows MOET from the FlowALP position, swaps to FUSDEV + // ----------------------------------------------------------------------- + case Type<@FUSDEVStrategy>(): + // Swappers: MOET <-> YIELD + let debtToYieldSwapper = self._createDebtToYieldSwapper(tokens: tokens, uniqueID: uniqueID) + let yieldToDebtSwapper = self._createYieldToDebtSwapper(tokens: tokens, uniqueID: uniqueID) + + // AutoBalancer-directed swap IO + let abaSwapSink = SwapConnectors.SwapSink( + swapper: debtToYieldSwapper, + sink: balancerIO.sink, + uniqueID: uniqueID + ) + let abaSwapSource = SwapConnectors.SwapSource( + swapper: yieldToDebtSwapper, + source: balancerIO.source, + uniqueID: uniqueID + ) - // Open FlowALPv0 position - let position <- self._openCreditPosition( - funds: <-withFunds, - issuanceSink: abaSwapSink, - repaymentSource: abaSwapSource - ) + // Open FlowALPv0 position + let position <- FlowYieldVaultsStrategiesV2._openCreditPosition( + funds: <-withFunds, + issuanceSink: abaSwapSink, + repaymentSource: abaSwapSource + ) - // Position Sink/Source (only Sink needed here, Source stays inside Strategy impl) - let positionSink = position.createSinkWithOptions(type: collateralType, pushToDrawDownSink: true) + // Position Sink/Source for collateral rebalancing + let positionSink = position.createSinkWithOptions(type: collateralType, pushToDrawDownSink: true) - // Yield -> Collateral swapper for recollateralization - let yieldToCollateralSwapper = self._createYieldToCollateralSwapper( - collateralConfig: collateralConfig, - yieldTokenEVMAddress: tokens.yieldTokenEVMAddress, - yieldTokenType: tokens.yieldTokenType, - collateralType: collateralType, - uniqueID: uniqueID - ) + // Yield -> Collateral swapper for recollateralization + let yieldToCollateralSwapper = self._createYieldToCollateralSwapper( + collateralConfig: collateralConfig, + yieldTokenEVMAddress: tokens.yieldTokenEVMAddress, + yieldTokenType: tokens.yieldTokenType, + collateralType: collateralType, + uniqueID: uniqueID + ) - let positionSwapSink = SwapConnectors.SwapSink( - swapper: yieldToCollateralSwapper, - sink: positionSink, - uniqueID: uniqueID - ) + let positionSwapSink = SwapConnectors.SwapSink( + swapper: yieldToCollateralSwapper, + sink: positionSink, + uniqueID: uniqueID + ) - // pullFromTopUpSource: false ensures Position maintains health buffer - // This prevents Position from being pushed to minHealth (1.1) limit - let positionSource = position.createSourceWithOptions( - type: collateralType, - pullFromTopUpSource: false // ← CONSERVATIVE: maintain safety buffer - ) + // pullFromTopUpSource: false ensures Position maintains health buffer + let positionSource = position.createSourceWithOptions( + type: collateralType, + pullFromTopUpSource: false + ) - // Create Collateral -> Yield swapper (reverse of yieldToCollateralSwapper) - // Allows AutoBalancer to pull collateral, swap to yield token - let collateralToYieldSwapper = self._createCollateralToYieldSwapper( - collateralConfig: collateralConfig, - yieldTokenEVMAddress: tokens.yieldTokenEVMAddress, - yieldTokenType: tokens.yieldTokenType, - collateralType: collateralType, - uniqueID: uniqueID - ) + // Collateral -> Yield swapper for AutoBalancer deficit recovery + let collateralToYieldSwapper = self._createCollateralToYieldSwapper( + collateralConfig: collateralConfig, + yieldTokenEVMAddress: tokens.yieldTokenEVMAddress, + yieldTokenType: tokens.yieldTokenType, + collateralType: collateralType, + uniqueID: uniqueID + ) - // Create Position swap source for AutoBalancer deficit recovery - // When AutoBalancer value drops below deposits, pulls collateral from Position - let positionSwapSource = SwapConnectors.SwapSource( - swapper: collateralToYieldSwapper, - source: positionSource, - uniqueID: uniqueID - ) + let positionSwapSource = SwapConnectors.SwapSource( + swapper: collateralToYieldSwapper, + source: positionSource, + uniqueID: uniqueID + ) - // Set AutoBalancer sink for overflow -> recollateralize - balancerIO.autoBalancer.setSink(positionSwapSink, updateSinkID: true) + balancerIO.autoBalancer.setSink(positionSwapSink, updateSinkID: true) + balancerIO.autoBalancer.setSource(positionSwapSource, updateSourceID: true) - // Set AutoBalancer source for deficit recovery -> pull from Position - balancerIO.autoBalancer.setSource(positionSwapSource, updateSourceID: true) + // Store yield→MOET swapper for later access during closePosition + FlowYieldVaultsStrategiesV2._setYieldToMoetSwapper(uniqueID.id, yieldToDebtSwapper) - // Store yield→MOET swapper in contract config for later access during closePosition - let yieldToMoetSwapperKey = FlowYieldVaultsStrategiesV2.getYieldToMoetSwapperConfigKey(uniqueID)! - FlowYieldVaultsStrategiesV2.config[yieldToMoetSwapperKey] = yieldToMoetSwapper + // Store MOET→collateral swapper for dust conversion in closePosition. + // Chain: MOET → FUSDEV (debtToYieldSwapper) → collateral (yieldToCollateralSwapper) + FlowYieldVaultsStrategiesV2._setDebtToCollateralSwapper( + uniqueID.id, + SwapConnectors.SequentialSwapper( + swappers: [debtToYieldSwapper, yieldToCollateralSwapper], + uniqueID: uniqueID + ) + ) - // @TODO implement moet to collateral swapper - // let moetToCollateralSwapperKey = FlowYieldVaultsStrategiesV2.getMoetToCollateralSwapperConfigKey(uniqueID) - // - // FlowYieldVaultsStrategiesV2.config[moetToCollateralSwapperKey] = moetToCollateralSwapper - // - switch type { - case Type<@FUSDEVStrategy>(): return <-create FUSDEVStrategy( id: uniqueID, collateralType: collateralType, position: <-position ) - case Type<@syWFLOWvStrategy>(): - return <-create syWFLOWvStrategy( - id: uniqueID, - collateralType: collateralType, - position: <-position - ) + default: panic("Unsupported strategy type \(type.identifier)") } @@ -568,68 +838,7 @@ access(all) contract FlowYieldVaultsStrategiesV2 { ) return strategyConfig[collateralType] - ?? panic( - "Could not find config for collateral \(collateralType.identifier) when creating Strategy \(strategyType.identifier)" - ) - } - - /// Gets the Pool's default token type (the borrowable token) - access(self) fun _getPoolDefaultToken(): Type { - let poolCap = FlowYieldVaultsStrategiesV2.account.storage.copy< - Capability - >(from: FlowALPv0.PoolCapStoragePath) - ?? panic("Missing or invalid pool capability") - - let poolRef = poolCap.borrow() ?? panic("Invalid Pool Cap") - return poolRef.getDefaultToken() - } - - access(self) fun _resolveTokenBundle( - collateralConfig: FlowYieldVaultsStrategiesV2.CollateralConfig - ): FlowYieldVaultsStrategiesV2.TokenBundle { - // Get the Pool's default token (the borrowable debt token) - // This could be MOET, USDC, or any other token the pool is configured to lend - let debtTokenType = self._getPoolDefaultToken() - let debtTokenEVMAddress = FlowEVMBridgeConfig.getEVMAddressAssociated(with: debtTokenType) - ?? panic("Token Vault type \(debtTokenType.identifier) has not yet been registered with the VMbridge") - - // YIELD (ERC4626 vault token) - let yieldTokenEVMAddress = collateralConfig.yieldTokenEVMAddress - let yieldTokenType = FlowEVMBridgeConfig.getTypeAssociated(with: yieldTokenEVMAddress) - ?? panic( - "Could not retrieve the VM Bridge associated Type for the yield token address \(yieldTokenEVMAddress.toString())" - ) - - // UNDERLYING asset of the ERC4626 vault - let underlying4626AssetEVMAddress = ERC4626Utils.underlyingAssetEVMAddress(vault: yieldTokenEVMAddress) - ?? panic( - "Could not get the underlying asset's EVM address for ERC4626Vault \(yieldTokenEVMAddress.toString())" - ) - let underlying4626AssetType = FlowEVMBridgeConfig.getTypeAssociated(with: underlying4626AssetEVMAddress) - ?? panic( - "Could not retrieve the VM Bridge associated Type for the ERC4626 underlying asset \(underlying4626AssetEVMAddress.toString())" - ) - - return FlowYieldVaultsStrategiesV2.TokenBundle( - debtTokenType: debtTokenType, - debtTokenEVMAddress: debtTokenEVMAddress, - yieldTokenType: yieldTokenType, - yieldTokenEVMAddress: yieldTokenEVMAddress, - underlying4626AssetType: underlying4626AssetType, - underlying4626AssetEVMAddress: underlying4626AssetEVMAddress - ) - } - - access(self) fun _createYieldTokenOracle( - yieldTokenEVMAddress: EVM.EVMAddress, - underlyingAssetType: Type, - uniqueID: DeFiActions.UniqueIdentifier - ): ERC4626PriceOracles.PriceOracle { - return ERC4626PriceOracles.PriceOracle( - vault: yieldTokenEVMAddress, - asset: underlyingAssetType, - uniqueID: uniqueID - ) + ?? panic("Could not find config for collateral \(collateralType.identifier)") } access(self) fun _createUniV3Swapper( @@ -653,56 +862,43 @@ access(all) contract FlowYieldVaultsStrategiesV2 { } access(self) fun _createDebtToYieldSwapper( - strategyType: Type, tokens: FlowYieldVaultsStrategiesV2.TokenBundle, uniqueID: DeFiActions.UniqueIdentifier ): SwapConnectors.MultiSwapper { - // Direct DEBT -> YIELD via AMM + // Direct MOET -> YIELD via AMM let debtToYieldAMM = self._createUniV3Swapper( - tokenPath: [tokens.debtTokenEVMAddress, tokens.yieldTokenEVMAddress], + tokenPath: [tokens.moetTokenEVMAddress, tokens.yieldTokenEVMAddress], feePath: [100], - inVault: tokens.debtTokenType, + inVault: tokens.moetTokenType, outVault: tokens.yieldTokenType, uniqueID: uniqueID ) - // DEBT -> UNDERLYING via AMM + // MOET -> UNDERLYING via AMM let debtToUnderlying = self._createUniV3Swapper( - tokenPath: [tokens.debtTokenEVMAddress, tokens.underlying4626AssetEVMAddress], + tokenPath: [tokens.moetTokenEVMAddress, tokens.underlying4626AssetEVMAddress], feePath: [100], - inVault: tokens.debtTokenType, + inVault: tokens.moetTokenType, outVault: tokens.underlying4626AssetType, uniqueID: uniqueID ) - // UNDERLYING -> YIELD via ERC4626 vault - // Morpho vaults use MorphoERC4626SwapConnectors; standard ERC4626 vaults use ERC4626SwapConnectors - var underlyingTo4626: {DeFiActions.Swapper}? = nil - if strategyType == Type<@FUSDEVStrategy>() { - underlyingTo4626 = MorphoERC4626SwapConnectors.Swapper( - vaultEVMAddress: tokens.yieldTokenEVMAddress, - coa: FlowYieldVaultsStrategiesV2._getCOACapability(), - feeSource: FlowYieldVaultsStrategiesV2._createFeeSource(withID: uniqueID), - uniqueID: uniqueID, - isReversed: false - ) - } else { - underlyingTo4626 = ERC4626SwapConnectors.Swapper( - asset: tokens.underlying4626AssetType, - vault: tokens.yieldTokenEVMAddress, - coa: FlowYieldVaultsStrategiesV2._getCOACapability(), - feeSource: FlowYieldVaultsStrategiesV2._createFeeSource(withID: uniqueID), - uniqueID: uniqueID - ) - } + // UNDERLYING -> YIELD via Morpho ERC4626 vault deposit + let underlyingTo4626 = MorphoERC4626SwapConnectors.Swapper( + vaultEVMAddress: tokens.yieldTokenEVMAddress, + coa: FlowYieldVaultsStrategiesV2._getCOACapability(), + feeSource: FlowYieldVaultsStrategiesV2._createFeeSource(withID: uniqueID), + uniqueID: uniqueID, + isReversed: false + ) let seq = SwapConnectors.SequentialSwapper( - swappers: [debtToUnderlying, underlyingTo4626!], + swappers: [debtToUnderlying, underlyingTo4626], uniqueID: uniqueID ) return SwapConnectors.MultiSwapper( - inVault: tokens.debtTokenType, + inVault: tokens.moetTokenType, outVault: tokens.yieldTokenType, swappers: [debtToYieldAMM, seq], uniqueID: uniqueID @@ -710,123 +906,57 @@ access(all) contract FlowYieldVaultsStrategiesV2 { } access(self) fun _createYieldToDebtSwapper( - strategyType: Type, tokens: FlowYieldVaultsStrategiesV2.TokenBundle, uniqueID: DeFiActions.UniqueIdentifier ): SwapConnectors.MultiSwapper { - // Direct YIELD -> DEBT via AMM + // Direct YIELD -> MOET via AMM let yieldToDebtAMM = self._createUniV3Swapper( - tokenPath: [tokens.yieldTokenEVMAddress, tokens.debtTokenEVMAddress], + tokenPath: [tokens.yieldTokenEVMAddress, tokens.moetTokenEVMAddress], feePath: [100], inVault: tokens.yieldTokenType, - outVault: tokens.debtTokenType, + outVault: tokens.moetTokenType, uniqueID: uniqueID ) - // Reverse path: Morpho vaults support direct redeem; standard ERC4626 vaults use AMM-only path - if strategyType == Type<@FUSDEVStrategy>() { - // YIELD -> UNDERLYING redeem via MorphoERC4626 vault - let yieldToUnderlying = MorphoERC4626SwapConnectors.Swapper( - vaultEVMAddress: tokens.yieldTokenEVMAddress, - coa: FlowYieldVaultsStrategiesV2._getCOACapability(), - feeSource: FlowYieldVaultsStrategiesV2._createFeeSource(withID: uniqueID), - uniqueID: uniqueID, - isReversed: true - ) - // UNDERLYING -> DEBT via AMM - let underlyingToDebt = self._createUniV3Swapper( - tokenPath: [tokens.underlying4626AssetEVMAddress, tokens.debtTokenEVMAddress], - feePath: [100], - inVault: tokens.underlying4626AssetType, - outVault: tokens.debtTokenType, - uniqueID: uniqueID - ) + // YIELD -> UNDERLYING redeem via MorphoERC4626 vault + let yieldToUnderlying = MorphoERC4626SwapConnectors.Swapper( + vaultEVMAddress: tokens.yieldTokenEVMAddress, + coa: FlowYieldVaultsStrategiesV2._getCOACapability(), + feeSource: FlowYieldVaultsStrategiesV2._createFeeSource(withID: uniqueID), + uniqueID: uniqueID, + isReversed: true + ) + // UNDERLYING -> MOET via AMM + let underlyingToDebt = self._createUniV3Swapper( + tokenPath: [tokens.underlying4626AssetEVMAddress, tokens.moetTokenEVMAddress], + feePath: [100], + inVault: tokens.underlying4626AssetType, + outVault: tokens.moetTokenType, + uniqueID: uniqueID + ) - let seq = SwapConnectors.SequentialSwapper( - swappers: [yieldToUnderlying, underlyingToDebt], - uniqueID: uniqueID - ) + let seq = SwapConnectors.SequentialSwapper( + swappers: [yieldToUnderlying, underlyingToDebt], + uniqueID: uniqueID + ) - return SwapConnectors.MultiSwapper( - inVault: tokens.yieldTokenType, - outVault: tokens.debtTokenType, - swappers: [yieldToDebtAMM, seq], - uniqueID: uniqueID - ) - } else { - // Standard ERC4626: AMM-only reverse (no synchronous redeem support) - return SwapConnectors.MultiSwapper( - inVault: tokens.yieldTokenType, - outVault: tokens.debtTokenType, - swappers: [yieldToDebtAMM], - uniqueID: uniqueID - ) - } + return SwapConnectors.MultiSwapper( + inVault: tokens.yieldTokenType, + outVault: tokens.moetTokenType, + swappers: [yieldToDebtAMM, seq], + uniqueID: uniqueID + ) } /// @TODO /// implement moet to collateral swapper // access(self) fun _createMoetToCollateralSwapper( - // strategyType: Type, // tokens: FlowYieldVaultsStrategiesV2.TokenBundle, // uniqueID: DeFiActions.UniqueIdentifier // ): SwapConnectors.MultiSwapper { // // Direct MOET -> underlying via AMM // } - access(self) fun _initAutoBalancerAndIO( - oracle: {DeFiActions.PriceOracle}, - yieldTokenType: Type, - recurringConfig: DeFiActions.AutoBalancerRecurringConfig?, - uniqueID: DeFiActions.UniqueIdentifier - ): FlowYieldVaultsStrategiesV2.AutoBalancerIO { - // NOTE: This stores the AutoBalancer in FlowYieldVaultsAutoBalancers storage and returns an authorized ref. - let autoBalancerRef = - FlowYieldVaultsAutoBalancers._initNewAutoBalancer( - oracle: oracle, - vaultType: yieldTokenType, - lowerThreshold: 0.95, - upperThreshold: 1.05, - rebalanceSink: nil, - rebalanceSource: nil, - recurringConfig: recurringConfig, - uniqueID: uniqueID - ) - - let sink = autoBalancerRef.createBalancerSink() - ?? panic("Could not retrieve Sink from AutoBalancer with id \(uniqueID.id)") - let source = autoBalancerRef.createBalancerSource() - ?? panic("Could not retrieve Source from AutoBalancer with id \(uniqueID.id)") - - return FlowYieldVaultsStrategiesV2.AutoBalancerIO( - autoBalancer: autoBalancerRef, - sink: sink, - source: source - ) - } - - access(self) fun _openCreditPosition( - funds: @{FungibleToken.Vault}, - issuanceSink: {DeFiActions.Sink}, - repaymentSource: {DeFiActions.Source} - ): @FlowALPv0.Position { - let poolCap = FlowYieldVaultsStrategiesV2.account.storage.copy< - Capability - >(from: FlowALPv0.PoolCapStoragePath) - ?? panic("Missing or invalid pool capability") - - let poolRef = poolCap.borrow() ?? panic("Invalid Pool Cap") - - let position <- poolRef.createPosition( - funds: <-funds, - issuanceSink: issuanceSink, - repaymentSource: repaymentSource, - pushToDrawDownSink: true - ) - - return <-position - } - access(self) fun _createYieldToCollateralSwapper( collateralConfig: FlowYieldVaultsStrategiesV2.CollateralConfig, yieldTokenEVMAddress: EVM.EVMAddress, @@ -891,6 +1021,191 @@ access(all) contract FlowYieldVaultsStrategiesV2 { } } + /// This StrategyComposer builds strategies that borrow the ERC4626 vault's own underlying + /// asset as debt (e.g. FLOW for syWFLOWv), depositing it directly via ERC4626 deposit/redeem + /// with no AMM swaps. FLOW (the underlying) cannot be used as collateral. + access(all) resource MoreERC4626StrategyComposer : FlowYieldVaults.StrategyComposer { + /// { Strategy Type: { Collateral Type: MoreERC4626CollateralConfig } } + access(self) let config: {Type: {Type: MoreERC4626CollateralConfig}} + + init(_ config: {Type: {Type: MoreERC4626CollateralConfig}}) { + self.config = config + } + + access(all) view fun getComposedStrategyTypes(): {Type: Bool} { + let composed: {Type: Bool} = {} + for t in self.config.keys { + composed[t] = true + } + return composed + } + + access(all) view fun getSupportedInitializationVaults(forStrategy: Type): {Type: Bool} { + let supported: {Type: Bool} = {} + if let strategyConfig = self.config[forStrategy] { + for collateralType in strategyConfig.keys { + supported[collateralType] = true + } + } + return supported + } + + access(self) view fun _supportsCollateral(forStrategy: Type, collateral: Type): Bool { + if let strategyConfig = self.config[forStrategy] { + return strategyConfig[collateral] != nil + } + return false + } + + access(all) view fun getSupportedInstanceVaults(forStrategy: Type, initializedWith: Type): {Type: Bool} { + return self._supportsCollateral(forStrategy: forStrategy, collateral: initializedWith) + ? { initializedWith: true } + : {} + } + + access(all) fun createStrategy( + _ type: Type, + uniqueID: DeFiActions.UniqueIdentifier, + withFunds: @{FungibleToken.Vault} + ): @{FlowYieldVaults.Strategy} { + pre { + self.config[type] != nil: "Unsupported strategy type \(type.identifier)" + } + + switch type { + case Type<@syWFLOWvStrategy>(): + let collateralType = withFunds.getType() + + let stratConfig = self.config[Type<@syWFLOWvStrategy>()] + ?? panic("Could not find config for strategy syWFLOWvStrategy") + let collateralConfig = stratConfig[collateralType] + ?? panic("Could not find config for collateral \(collateralType.identifier) when creating syWFLOWvStrategy") + + let tokens = FlowYieldVaultsStrategiesV2._resolveTokenBundle( + yieldTokenEVMAddress: collateralConfig.yieldTokenEVMAddress + ) + + // Reject FLOW as collateral — it is the vault's underlying / debt token + assert( + collateralType != tokens.underlying4626AssetType, + message: "syWFLOWvStrategy: FLOW cannot be used as collateral — it is the vault's underlying asset" + ) + + let yieldTokenOracle = FlowYieldVaultsStrategiesV2._createYieldTokenOracle( + yieldTokenEVMAddress: tokens.yieldTokenEVMAddress, + underlyingAssetType: tokens.underlying4626AssetType, + uniqueID: uniqueID + ) + + let recurringConfig = FlowYieldVaultsStrategiesV2._createRecurringConfig(withID: uniqueID) + + let balancerIO = FlowYieldVaultsStrategiesV2._initAutoBalancerAndIO( + oracle: yieldTokenOracle, + yieldTokenType: tokens.yieldTokenType, + recurringConfig: recurringConfig, + uniqueID: uniqueID + ) + + // For syWFLOWvStrategy the debt token IS the underlying asset (FLOW), not MOET. + // Use tokens.underlying4626AssetType directly wherever the debt token type is needed. + 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) + + // FLOW→collateral swapper for dust conversion in closePosition + let flowToCollateral = UniswapV3SwapConnectors.Swapper( + factoryAddress: FlowYieldVaultsStrategiesV2.univ3FactoryEVMAddress, + routerAddress: FlowYieldVaultsStrategiesV2.univ3RouterEVMAddress, + quoterAddress: FlowYieldVaultsStrategiesV2.univ3QuoterEVMAddress, + tokenPath: collateralConfig.debtToCollateralUniV3AddressPath, + feePath: collateralConfig.debtToCollateralUniV3FeePath, + inVault: tokens.underlying4626AssetType, + outVault: collateralType, + coaCapability: FlowYieldVaultsStrategiesV2._getCOACapability(), + uniqueID: uniqueID + ) + + // Store debtTokenType in contract-level config (resource field removed for upgrade compat) + FlowYieldVaultsStrategiesV2._setSyWFLOWvDebtTokenType(uniqueID.id, flowDebtTokenType) + + return <-create syWFLOWvStrategy( + id: uniqueID, + collateralType: collateralType, + position: <-positionFlow, + yieldToDebtSwapper: syWFLOWvToFlow, + debtToCollateralSwapper: flowToCollateral + ) + + default: + panic("Unsupported strategy type \(type.identifier)") + } + } + } + access(all) entitlement Configure access(self) @@ -914,14 +1229,45 @@ access(all) contract FlowYieldVaultsStrategiesV2 { yieldToCollateralUniV3FeePath: yieldToCollateralFeePath ) } + + access(self) + fun makeMoreERC4626CollateralConfig( + yieldTokenEVMAddress: EVM.EVMAddress, + yieldToUnderlyingAddressPath: [EVM.EVMAddress], + yieldToUnderlyingFeePath: [UInt32], + debtToCollateralAddressPath: [EVM.EVMAddress], + debtToCollateralFeePath: [UInt32] + ): MoreERC4626CollateralConfig { + pre { + yieldToUnderlyingAddressPath.length > 1: + "Invalid Uniswap V3 swap path length" + yieldToUnderlyingFeePath.length == yieldToUnderlyingAddressPath.length - 1: + "Uniswap V3 fee path length must be path length - 1" + yieldToUnderlyingAddressPath[0].equals(yieldTokenEVMAddress): + "UniswapV3 swap path must start with yield token" + debtToCollateralAddressPath.length > 1: + "Invalid debt-to-collateral Uniswap V3 path length" + debtToCollateralFeePath.length == debtToCollateralAddressPath.length - 1: + "Debt-to-collateral Uniswap V3 fee path length must be path length - 1" + } + return MoreERC4626CollateralConfig( + yieldTokenEVMAddress: yieldTokenEVMAddress, + yieldToUnderlyingUniV3AddressPath: yieldToUnderlyingAddressPath, + yieldToUnderlyingUniV3FeePath: yieldToUnderlyingFeePath, + debtToCollateralUniV3AddressPath: debtToCollateralAddressPath, + debtToCollateralUniV3FeePath: debtToCollateralFeePath + ) + } + /// This resource enables the issuance of StrategyComposers, thus safeguarding the issuance of Strategies which /// may utilize resource consumption (i.e. account storage). Since Strategy creation consumes account storage /// via configured AutoBalancers access(all) resource StrategyComposerIssuer : FlowYieldVaults.StrategyComposerIssuer { - /// { StrategyComposer Type: { Strategy Type: { Collateral Type: FlowYieldVaultsStrategiesV2.CollateralConfig } } } - access(all) var configs: {Type: {Type: {Type: FlowYieldVaultsStrategiesV2.CollateralConfig}}} + /// { Composer Type: { Strategy Type: { Collateral Type: CollateralConfig } } } + /// Used by MorphoERC4626StrategyComposer. + access(all) var configs: {Type: {Type: {Type: CollateralConfig}}} - init(configs: {Type: {Type: {Type: FlowYieldVaultsStrategiesV2.CollateralConfig}}}) { + init(configs: {Type: {Type: {Type: CollateralConfig}}}) { self.configs = configs } @@ -930,49 +1276,76 @@ access(all) contract FlowYieldVaultsStrategiesV2 { strategy: Type, collateral: Type ): Bool { - if let composerConfig = self.configs[composer] { - if let strategyConfig = composerConfig[strategy] { - return strategyConfig[collateral] != nil + if let composerPartition = self.configs[composer] { + if let stratPartition = composerPartition[strategy] { + if stratPartition[collateral] != nil { return true } } } - return false + return FlowYieldVaultsStrategiesV2._getMoreERC4626Config( + composer: composer, strategy: strategy, collateral: collateral + ) != nil } access(all) view fun getSupportedComposers(): {Type: Bool} { - return { - Type<@MorphoERC4626StrategyComposer>(): true + return { + Type<@MorphoERC4626StrategyComposer>(): true, + Type<@MoreERC4626StrategyComposer>(): true } } access(self) view fun isSupportedComposer(_ type: Type): Bool { return type == Type<@MorphoERC4626StrategyComposer>() + || type == Type<@MoreERC4626StrategyComposer>() } + access(all) fun issueComposer(_ type: Type): @{FlowYieldVaults.StrategyComposer} { pre { - self.isSupportedComposer(type) == true: - "Unsupported StrategyComposer \(type.identifier) requested" - self.configs[type] != nil: - "Could not find config for StrategyComposer \(type.identifier)" + self.isSupportedComposer(type): "Unsupported StrategyComposer \(type.identifier) requested" } switch type { case Type<@MorphoERC4626StrategyComposer>(): - return <- create MorphoERC4626StrategyComposer(self.configs[type]!) + return <- create MorphoERC4626StrategyComposer(self.configs[type] ?? {}) + case Type<@MoreERC4626StrategyComposer>(): + return <- create MoreERC4626StrategyComposer( + FlowYieldVaultsStrategiesV2._getMoreERC4626ComposerConfig(type) + ) default: panic("Unsupported StrategyComposer \(type.identifier) requested") } } + /// Merges new CollateralConfig entries into the MorphoERC4626StrategyComposer config. access(Configure) - fun upsertConfigFor( - composer: Type, + fun upsertMorphoConfig( config: {Type: {Type: FlowYieldVaultsStrategiesV2.CollateralConfig}} ) { - pre { - self.isSupportedComposer(composer) == true: - "Unsupported StrategyComposer Type \(composer.identifier)" + for stratType in config.keys { + assert(stratType.isSubtype(of: Type<@{FlowYieldVaults.Strategy}>()), + message: "Invalid config key \(stratType.identifier) - not a FlowYieldVaults.Strategy Type") + for collateralType in config[stratType]!.keys { + assert(collateralType.isSubtype(of: Type<@{FungibleToken.Vault}>()), + message: "Invalid config key at config[\(stratType.identifier)] - \(collateralType.identifier) is not a FungibleToken.Vault") + } + } + + let composerType = Type<@MorphoERC4626StrategyComposer>() + var composerPartition = self.configs[composerType] ?? {} + for stratType in config.keys { + var stratPartition: {Type: CollateralConfig} = composerPartition[stratType] ?? {} + let newPerCollateral = config[stratType]! + for collateralType in newPerCollateral.keys { + stratPartition[collateralType] = newPerCollateral[collateralType]! + } + composerPartition[stratType] = stratPartition } + self.configs[composerType] = composerPartition + } - // Validate keys + /// 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") @@ -982,26 +1355,21 @@ access(all) contract FlowYieldVaultsStrategiesV2 { } } - // Merge instead of overwrite - let existingComposerConfig = self.configs[composer] ?? {} - var mergedComposerConfig = existingComposerConfig - + let composerType = Type<@MoreERC4626StrategyComposer>() for stratType in config.keys { let newPerCollateral = config[stratType]! - let existingPerCollateral = mergedComposerConfig[stratType] ?? {} - var mergedPerCollateral: {Type: FlowYieldVaultsStrategiesV2.CollateralConfig} = existingPerCollateral - for collateralType in newPerCollateral.keys { - mergedPerCollateral[collateralType] = newPerCollateral[collateralType]! + FlowYieldVaultsStrategiesV2._setMoreERC4626Config( + composer: composerType, + strategy: stratType, + collateral: collateralType, + cfg: newPerCollateral[collateralType]! + ) } - mergedComposerConfig[stratType] = mergedPerCollateral } - - self.configs[composer] = mergedComposerConfig } - access(Configure) fun addOrUpdateCollateralConfig( - composer: Type, + access(Configure) fun addOrUpdateMorphoCollateralConfig( strategyType: Type, collateralVaultType: Type, yieldTokenEVMAddress: EVM.EVMAddress, @@ -1009,40 +1377,69 @@ access(all) contract FlowYieldVaultsStrategiesV2 { yieldToCollateralFeePath: [UInt32] ) { pre { - self.isSupportedComposer(composer) == true: - "Unsupported StrategyComposer Type \(composer.identifier)" strategyType.isSubtype(of: Type<@{FlowYieldVaults.Strategy}>()): "Strategy type \(strategyType.identifier) is not a FlowYieldVaults.Strategy" collateralVaultType.isSubtype(of: Type<@{FungibleToken.Vault}>()): "Collateral type \(collateralVaultType.identifier) is not a FungibleToken.Vault" } - // Base struct with shared addresses - var base = FlowYieldVaultsStrategiesV2.makeCollateralConfig( + let base = FlowYieldVaultsStrategiesV2.makeCollateralConfig( yieldTokenEVMAddress: yieldTokenEVMAddress, yieldToCollateralAddressPath: yieldToCollateralAddressPath, yieldToCollateralFeePath: yieldToCollateralFeePath ) + self.upsertMorphoConfig(config: { strategyType: { collateralVaultType: base } }) + } - // Wrap into the nested config expected by upsertConfigFor - let singleCollateralConfig = { - strategyType: { - collateralVaultType: base - } + access(Configure) fun addOrUpdateMoreERC4626CollateralConfig( + strategyType: Type, + collateralVaultType: Type, + yieldTokenEVMAddress: EVM.EVMAddress, + yieldToUnderlyingAddressPath: [EVM.EVMAddress], + yieldToUnderlyingFeePath: [UInt32], + debtToCollateralAddressPath: [EVM.EVMAddress], + debtToCollateralFeePath: [UInt32] + ) { + pre { + strategyType.isSubtype(of: Type<@{FlowYieldVaults.Strategy}>()): + "Strategy type \(strategyType.identifier) is not a FlowYieldVaults.Strategy" + collateralVaultType.isSubtype(of: Type<@{FungibleToken.Vault}>()): + "Collateral type \(collateralVaultType.identifier) is not a FungibleToken.Vault" } - self.upsertConfigFor(composer: composer, config: singleCollateralConfig) + let cfg = FlowYieldVaultsStrategiesV2.makeMoreERC4626CollateralConfig( + yieldTokenEVMAddress: yieldTokenEVMAddress, + yieldToUnderlyingAddressPath: yieldToUnderlyingAddressPath, + yieldToUnderlyingFeePath: yieldToUnderlyingFeePath, + debtToCollateralAddressPath: debtToCollateralAddressPath, + debtToCollateralFeePath: debtToCollateralFeePath + ) + self.upsertMoreERC4626Config(config: { strategyType: { collateralVaultType: cfg } }) } + access(Configure) fun purgeConfig() { self.configs = { Type<@MorphoERC4626StrategyComposer>(): { - Type<@FUSDEVStrategy>(): {} as {Type: FlowYieldVaultsStrategiesV2.CollateralConfig}, - Type<@syWFLOWvStrategy>(): {} as {Type: FlowYieldVaultsStrategiesV2.CollateralConfig} + Type<@FUSDEVStrategy>(): {} as {Type: CollateralConfig} } } + FlowYieldVaultsStrategiesV2._purgeMoreERC4626Configs() } } + /// Creates a fresh StrategyComposerIssuer with the default config skeleton. + /// Intended for the deployer account to recreate a lost or destroyed issuer via a transaction. + access(all) + fun createIssuer(): @StrategyComposerIssuer { + return <- create StrategyComposerIssuer( + configs: { + Type<@MorphoERC4626StrategyComposer>(): { + Type<@FUSDEVStrategy>(): {} as {Type: CollateralConfig} + } + } + ) + } + /// Returns the COA capability for this account /// TODO: this is temporary until we have a better way to pass user's COAs to inner connectors access(self) @@ -1077,7 +1474,7 @@ access(all) contract FlowYieldVaultsStrategiesV2 { fun _createRecurringConfig(withID: DeFiActions.UniqueIdentifier?): DeFiActions.AutoBalancerRecurringConfig { // Create txnFunder that can provide/accept FLOW for scheduling fees let txnFunder = self._createTxnFunder(withID: withID) - + return DeFiActions.AutoBalancerRecurringConfig( interval: 60 * 10, // Rebalance every 10 minutes priority: FlowTransactionScheduler.Priority.Medium, @@ -1105,18 +1502,124 @@ access(all) contract FlowYieldVaultsStrategiesV2 { ) } - access(self) view fun getYieldToMoetSwapperConfigKey(_ uniqueID: DeFiActions.UniqueIdentifier?): String { - pre { - uniqueID != nil: "Missing UniqueIdentifier for swapper config key" + // --- "yieldToMoetSwappers" partition --- + + access(contract) view fun _getYieldToMoetSwapper(_ id: UInt64): {DeFiActions.Swapper}? { + let partition = FlowYieldVaultsStrategiesV2.config["yieldToMoetSwappers"] as! {UInt64: {DeFiActions.Swapper}}? ?? {} + return partition[id] + } + + access(contract) fun _setYieldToMoetSwapper(_ id: UInt64, _ swapper: {DeFiActions.Swapper}) { + var partition = FlowYieldVaultsStrategiesV2.config["yieldToMoetSwappers"] as! {UInt64: {DeFiActions.Swapper}}? ?? {} + partition[id] = swapper + FlowYieldVaultsStrategiesV2.config["yieldToMoetSwappers"] = partition + } + + // --- "debtToCollateralSwappers" partition --- + + access(contract) view fun _getDebtToCollateralSwapper(_ id: UInt64): {DeFiActions.Swapper}? { + let partition = FlowYieldVaultsStrategiesV2.config["debtToCollateralSwappers"] as! {UInt64: {DeFiActions.Swapper}}? ?? {} + return partition[id] + } + + access(contract) fun _setDebtToCollateralSwapper(_ id: UInt64, _ swapper: {DeFiActions.Swapper}) { + var partition = FlowYieldVaultsStrategiesV2.config["debtToCollateralSwappers"] as! {UInt64: {DeFiActions.Swapper}}? ?? {} + partition[id] = swapper + FlowYieldVaultsStrategiesV2.config["debtToCollateralSwappers"] = partition + } + + // --- "closedPositions" partition --- + + access(contract) view fun _isPositionClosed(_ uniqueID: DeFiActions.UniqueIdentifier?): Bool { + if uniqueID == nil { return false } + let partition = FlowYieldVaultsStrategiesV2.config["closedPositions"] as! {UInt64: Bool}? ?? {} + return partition[uniqueID!.id] ?? false + } + + access(contract) fun _markPositionClosed(_ uniqueID: DeFiActions.UniqueIdentifier?) { + if let id = uniqueID { + var partition = FlowYieldVaultsStrategiesV2.config["closedPositions"] as! {UInt64: Bool}? ?? {} + partition[id.id] = true + FlowYieldVaultsStrategiesV2.config["closedPositions"] = partition } - return "yieldToMoetSwapper_\(uniqueID!.id.toString())" } - access(self) view fun getMoetToCollateralSwapperConfigKey(_ uniqueID: DeFiActions.UniqueIdentifier?): String { - pre { - uniqueID != nil: "Missing UniqueIdentifier for swapper config key" + access(contract) fun _cleanupPositionClosed(_ uniqueID: DeFiActions.UniqueIdentifier?) { + if let id = uniqueID { + var partition = FlowYieldVaultsStrategiesV2.config["closedPositions"] as! {UInt64: Bool}? ?? {} + partition.remove(key: id.id) + FlowYieldVaultsStrategiesV2.config["closedPositions"] = partition + } + } + + // --- "syWFLOWvDebtTokenTypes" partition --- + // Stores the debt token Type per syWFLOWvStrategy uniqueID. + // Kept in the contract-level config map so no new field is added to the deployed syWFLOWvStrategy resource. + + access(contract) view fun _getSyWFLOWvDebtTokenType(_ id: UInt64): Type? { + let partition = FlowYieldVaultsStrategiesV2.config["syWFLOWvDebtTokenTypes"] as! {UInt64: Type}? ?? {} + return partition[id] + } + + access(contract) fun _setSyWFLOWvDebtTokenType(_ id: UInt64, _ t: Type) { + var partition = FlowYieldVaultsStrategiesV2.config["syWFLOWvDebtTokenTypes"] as! {UInt64: Type}? ?? {} + partition[id] = t + FlowYieldVaultsStrategiesV2.config["syWFLOWvDebtTokenTypes"] = partition + } + + access(contract) fun _removeSyWFLOWvDebtTokenType(_ id: UInt64?) { + if let unwrappedID = id { + var partition = FlowYieldVaultsStrategiesV2.config["syWFLOWvDebtTokenTypes"] as! {UInt64: Type}? ?? {} + partition.remove(key: unwrappedID) + FlowYieldVaultsStrategiesV2.config["syWFLOWvDebtTokenTypes"] = partition } - return "moetToCollateralSwapper_\(uniqueID!.id.toString())" + } + + // --- "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( @@ -1135,13 +1638,14 @@ access(all) contract FlowYieldVaultsStrategiesV2 { panic("Could not find EVM address for \(moetType.identifier) - ensure the asset is onboarded to the VM Bridge") } - let configs = { + let issuer <- create StrategyComposerIssuer( + configs: { Type<@MorphoERC4626StrategyComposer>(): { - Type<@FUSDEVStrategy>(): {} as {Type: FlowYieldVaultsStrategiesV2.CollateralConfig}, - Type<@syWFLOWvStrategy>(): {} as {Type: FlowYieldVaultsStrategiesV2.CollateralConfig} + Type<@FUSDEVStrategy>(): {} as {Type: CollateralConfig} } } - self.account.storage.save(<-create StrategyComposerIssuer(configs: configs), to: self.IssuerStoragePath) + ) + self.account.storage.save(<-issuer, to: self.IssuerStoragePath) // TODO: this is temporary until we have a better way to pass user's COAs to inner connectors // create a COA in this account diff --git a/cadence/tests/FlowYieldVaultsStrategiesV2_syWFLOWv_test.cdc b/cadence/tests/FlowYieldVaultsStrategiesV2_syWFLOWv_test.cdc new file mode 100644 index 00000000..8dc9e022 --- /dev/null +++ b/cadence/tests/FlowYieldVaultsStrategiesV2_syWFLOWv_test.cdc @@ -0,0 +1,515 @@ +#test_fork(network: "mainnet", height: nil) + +import Test + +import "EVM" +import "FlowToken" +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 + +/* --- 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 ".concat(user.address.toString())) + return ids![ids!.length - 1] +} + +/* --- 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 FlowYieldVaultsAutoBalancers...") + err = Test.deployContract( + name: "FlowYieldVaultsAutoBalancers", + path: "../../cadence/contracts/FlowYieldVaultsAutoBalancers.cdc", + arguments: [] + ) + Test.expect(err, Test.beNil()) + + log("Deploying FlowYieldVaultsStrategiesV2...") + err = Test.deployContract( + name: "FlowYieldVaultsStrategiesV2", + path: "../../cadence/contracts/FlowYieldVaultsStrategiesV2.cdc", + arguments: [ + "0xca6d7Bb03334bBf135902e1d919a5feccb461632", + "0xeEDC6Ff75e1b10B903D9013c358e446a73d35341", + "0x370A8DF17742867a44e56223EC20D82092242C85" + ] + ) + Test.expect(err, Test.beNil()) + + // Recreate the StrategyComposerIssuer (deleted from mainnet storage). + log("Recreating StrategyComposerIssuer...") + var result = _executeTransactionFile( + "../transactions/flow-yield-vaults/admin/recreate_composer_issuer.cdc", + [], + [adminAccount] + ) + Test.expect(result, Test.beSucceeded()) + + // 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 500)...") + result = _executeTransactionFile( + "../transactions/flow-yield-vaults/admin/upsert_more_erc4626_config.cdc", + [ + syWFLOWvStrategyIdentifier, + pyusd0VaultIdentifier, + syWFLOWvEVMAddress, + [syWFLOWvEVMAddress, wflowEVMAddress], // yieldToUnderlying + [100 as UInt32], + [wflowEVMAddress, pyusd0EVMAddress], // debtToCollateral + [500 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()) + + // 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() { + log("Creating syWFLOWvStrategy yield vault with 1.0 PYUSD0...") + let result = _executeTransactionFile( + "../transactions/flow-yield-vaults/create_yield_vault.cdc", + [syWFLOWvStrategyIdentifier, pyusd0VaultIdentifier, 1.0], + [pyusd0User] + ) + Test.expect(result, Test.beSucceeded()) + + pyusd0VaultID = _latestVaultID(pyusd0User) + log("Created PYUSD0 vault ID: ".concat(pyusd0VaultID.toString())) + + 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(balance != nil && balance! > 0.0, message: "Expected positive balance after create (PYUSD0)") + log("PYUSD0 vault balance after create: ".concat(balance!.toString())) +} + +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 ".concat(pyusd0VaultID.toString()).concat("...")) + 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: 0.01), + message: "PYUSD0 deposit: expected ~".concat((before + depositAmount).toString()).concat(", got ").concat(after.toString())) + log("PYUSD0 vault balance after deposit: ".concat(after.toString())) +} + +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 ".concat(pyusd0VaultID.toString()).concat("...")) + 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: 0.01), + message: "PYUSD0 withdraw: expected ~".concat((before - withdrawAmount).toString()).concat(", got ").concat(after.toString())) + log("PYUSD0 vault balance after withdrawal: ".concat(after.toString())) +} + +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 + log("Closing PYUSD0 vault ".concat(pyusd0VaultID.toString()).concat(" (balance: ").concat(vaultBalBefore.toString()).concat(")...")) + 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") + log("PYUSD0 yield vault closed successfully") +} + +/* ========================================================= + WBTC collateral lifecycle + ========================================================= */ + +access(all) fun testCreateSyWFLOWvYieldVault_WBTC() { + log("Creating syWFLOWvStrategy yield vault with 0.0001 WBTC...") + let result = _executeTransactionFile( + "../transactions/flow-yield-vaults/create_yield_vault.cdc", + [syWFLOWvStrategyIdentifier, wbtcVaultIdentifier, 0.0001], + [wbtcUser] + ) + Test.expect(result, Test.beSucceeded()) + + wbtcVaultID = _latestVaultID(wbtcUser) + log("Created WBTC vault ID: ".concat(wbtcVaultID.toString())) + + let bal = _executeScript("../scripts/flow-yield-vaults/get_yield_vault_balance.cdc", [wbtcUser.address, wbtcVaultID]) + Test.expect(bal, Test.beSucceeded()) + let balance = bal.returnValue! as! UFix64? + Test.assert(balance != nil && balance! > 0.0, message: "Expected positive balance after create (WBTC)") + log("WBTC vault balance after create: ".concat(balance!.toString())) +} + +access(all) fun 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 ".concat(wbtcVaultID.toString()).concat("...")) + Test.expect( + _executeTransactionFile("../transactions/flow-yield-vaults/deposit_to_yield_vault.cdc", [wbtcVaultID, depositAmount], [wbtcUser]), + Test.beSucceeded() + ) + let after = (_executeScript("../scripts/flow-yield-vaults/get_yield_vault_balance.cdc", [wbtcUser.address, wbtcVaultID]).returnValue! as! UFix64?)! + Test.assert(equalAmounts(a: after, b: before + depositAmount, tolerance: 0.000005), + message: "WBTC deposit: expected ~".concat((before + depositAmount).toString()).concat(", got ").concat(after.toString())) + log("WBTC vault balance after deposit: ".concat(after.toString())) +} + +access(all) fun 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 ".concat(wbtcVaultID.toString()).concat("...")) + Test.expect( + _executeTransactionFile("../transactions/flow-yield-vaults/withdraw_from_yield_vault.cdc", [wbtcVaultID, withdrawAmount], [wbtcUser]), + Test.beSucceeded() + ) + let after = (_executeScript("../scripts/flow-yield-vaults/get_yield_vault_balance.cdc", [wbtcUser.address, wbtcVaultID]).returnValue! as! UFix64?)! + Test.assert(equalAmounts(a: after, b: before - withdrawAmount, tolerance: 0.000005), + message: "WBTC withdraw: expected ~".concat((before - withdrawAmount).toString()).concat(", got ").concat(after.toString())) + log("WBTC vault balance after withdrawal: ".concat(after.toString())) +} + +access(all) fun testCloseSyWFLOWvYieldVault_WBTC() { + let vaultBalBefore = (_executeScript("../scripts/flow-yield-vaults/get_yield_vault_balance.cdc", [wbtcUser.address, wbtcVaultID]).returnValue! as! UFix64?) ?? 0.0 + log("Closing WBTC vault ".concat(wbtcVaultID.toString()).concat(" (balance: ").concat(vaultBalBefore.toString()).concat(")...")) + Test.expect( + _executeTransactionFile("../transactions/flow-yield-vaults/close_yield_vault.cdc", [wbtcVaultID], [wbtcUser]), + Test.beSucceeded() + ) + let vaultBalAfter = _executeScript("../scripts/flow-yield-vaults/get_yield_vault_balance.cdc", [wbtcUser.address, wbtcVaultID]) + Test.expect(vaultBalAfter, Test.beSucceeded()) + Test.assert(vaultBalAfter.returnValue == nil, message: "WBTC vault should no longer exist after close") + log("WBTC yield vault closed successfully") +} + +/* ========================================================= + WETH collateral lifecycle + ========================================================= */ + +access(all) fun testCreateSyWFLOWvYieldVault_WETH() { + log("Creating syWFLOWvStrategy yield vault with 0.001 WETH...") + let result = _executeTransactionFile( + "../transactions/flow-yield-vaults/create_yield_vault.cdc", + [syWFLOWvStrategyIdentifier, wethVaultIdentifier, 0.001], + [wethUser] + ) + Test.expect(result, Test.beSucceeded()) + + wethVaultID = _latestVaultID(wethUser) + log("Created WETH vault ID: ".concat(wethVaultID.toString())) + + let bal = _executeScript("../scripts/flow-yield-vaults/get_yield_vault_balance.cdc", [wethUser.address, wethVaultID]) + Test.expect(bal, Test.beSucceeded()) + let balance = bal.returnValue! as! UFix64? + Test.assert(balance != nil && balance! > 0.0, message: "Expected positive balance after create (WETH)") + log("WETH vault balance after create: ".concat(balance!.toString())) +} + +access(all) fun 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 ".concat(wethVaultID.toString()).concat("...")) + Test.expect( + _executeTransactionFile("../transactions/flow-yield-vaults/deposit_to_yield_vault.cdc", [wethVaultID, depositAmount], [wethUser]), + Test.beSucceeded() + ) + let after = (_executeScript("../scripts/flow-yield-vaults/get_yield_vault_balance.cdc", [wethUser.address, wethVaultID]).returnValue! as! UFix64?)! + Test.assert(equalAmounts(a: after, b: before + depositAmount, tolerance: 0.00005), + message: "WETH deposit: expected ~".concat((before + depositAmount).toString()).concat(", got ").concat(after.toString())) + log("WETH vault balance after deposit: ".concat(after.toString())) +} + +access(all) fun 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 ".concat(wethVaultID.toString()).concat("...")) + Test.expect( + _executeTransactionFile("../transactions/flow-yield-vaults/withdraw_from_yield_vault.cdc", [wethVaultID, withdrawAmount], [wethUser]), + Test.beSucceeded() + ) + let after = (_executeScript("../scripts/flow-yield-vaults/get_yield_vault_balance.cdc", [wethUser.address, wethVaultID]).returnValue! as! UFix64?)! + Test.assert(equalAmounts(a: after, b: before - withdrawAmount, tolerance: 0.00005), + message: "WETH withdraw: expected ~".concat((before - withdrawAmount).toString()).concat(", got ").concat(after.toString())) + log("WETH vault balance after withdrawal: ".concat(after.toString())) +} + +access(all) fun testCloseSyWFLOWvYieldVault_WETH() { + let vaultBalBefore = (_executeScript("../scripts/flow-yield-vaults/get_yield_vault_balance.cdc", [wethUser.address, wethVaultID]).returnValue! as! UFix64?) ?? 0.0 + log("Closing WETH vault ".concat(wethVaultID.toString()).concat(" (balance: ").concat(vaultBalBefore.toString()).concat(")...")) + Test.expect( + _executeTransactionFile("../transactions/flow-yield-vaults/close_yield_vault.cdc", [wethVaultID], [wethUser]), + Test.beSucceeded() + ) + let vaultBalAfter = _executeScript("../scripts/flow-yield-vaults/get_yield_vault_balance.cdc", [wethUser.address, wethVaultID]) + Test.expect(vaultBalAfter, Test.beSucceeded()) + Test.assert(vaultBalAfter.returnValue == nil, message: "WETH vault should no longer exist after close") + log("WETH yield vault closed successfully") +} diff --git a/cadence/tests/PMStrategiesV1_syWFLOWv_test.cdc b/cadence/tests/PMStrategiesV1_syWFLOWv_test.cdc index 7a181a20..6de7cdfa 100644 --- a/cadence/tests/PMStrategiesV1_syWFLOWv_test.cdc +++ b/cadence/tests/PMStrategiesV1_syWFLOWv_test.cdc @@ -8,8 +8,6 @@ import "FlowYieldVaults" import "PMStrategiesV1" import "FlowYieldVaultsClosedBeta" -import "test_helpers.cdc" - /// Fork test for PMStrategiesV1 syWFLOWv strategy — validates the full YieldVault lifecycle /// (create, deposit, withdraw, close) against real mainnet state. /// @@ -55,6 +53,11 @@ access(all) var syWFLOWvYieldVaultID: UInt64 = 0 /* --- Test 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( @@ -66,6 +69,14 @@ fun _executeTransactionFile(_ path: String, _ args: [AnyStruct], _ signers: [Tes 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 +} + /* --- Setup --- */ access(all) fun setup() { diff --git a/cadence/tests/transactions/provision_wbtc_from_weth.cdc b/cadence/tests/transactions/provision_wbtc_from_weth.cdc new file mode 100644 index 00000000..bc7bedc3 --- /dev/null +++ b/cadence/tests/transactions/provision_wbtc_from_weth.cdc @@ -0,0 +1,96 @@ +import "FungibleToken" +import "FungibleTokenMetadataViews" +import "EVM" +import "UniswapV3SwapConnectors" +import "FlowEVMBridgeConfig" + +/// Swap WETH Cadence tokens → WBTC Cadence tokens via the UniV3 WETH/WBTC pool. +/// Sets up the WBTC Cadence vault in signer's storage if not present. +/// +/// @param factoryAddr: UniswapV3 factory EVM address (hex, no 0x prefix) +/// @param routerAddr: UniswapV3 router EVM address +/// @param quoterAddr: UniswapV3 quoter EVM address +/// @param wethEvmAddr: WETH EVM contract address +/// @param wbtcEvmAddr: WBTC (cbBTC) EVM contract address +/// @param fee: UniV3 pool fee tier (e.g. 3000) +/// @param wethAmount: Amount of WETH (Cadence UFix64) to swap for WBTC +/// +transaction( + factoryAddr: String, + routerAddr: String, + quoterAddr: String, + wethEvmAddr: String, + wbtcEvmAddr: String, + fee: UInt32, + wethAmount: UFix64 +) { + prepare(signer: auth(Storage, BorrowValue, IssueStorageCapabilityController, SaveValue, PublishCapability, UnpublishCapability) &Account) { + let coaCap = signer.capabilities.storage.issue(/storage/evm) + + let wethEVM = EVM.addressFromString(wethEvmAddr) + let wbtcEVM = EVM.addressFromString(wbtcEvmAddr) + + let wethType = FlowEVMBridgeConfig.getTypeAssociated(with: wethEVM) + ?? panic("WETH EVM address not registered in bridge config: ".concat(wethEvmAddr)) + let wbtcType = FlowEVMBridgeConfig.getTypeAssociated(with: wbtcEVM) + ?? panic("WBTC EVM address not registered in bridge config: ".concat(wbtcEvmAddr)) + + let swapper = UniswapV3SwapConnectors.Swapper( + factoryAddress: EVM.addressFromString(factoryAddr), + routerAddress: EVM.addressFromString(routerAddr), + quoterAddress: EVM.addressFromString(quoterAddr), + tokenPath: [wethEVM, wbtcEVM], + feePath: [fee], + inVault: wethType, + outVault: wbtcType, + coaCapability: coaCap, + uniqueID: nil + ) + + // Locate WETH vault via FTVaultData so we don't hard-code the storage path. + let wethVaultCompType = CompositeType(wethType.identifier) + ?? panic("Cannot construct CompositeType for WETH: ".concat(wethType.identifier)) + let wethContract = getAccount(wethVaultCompType.address!).contracts.borrow<&{FungibleToken}>(name: wethVaultCompType.contractName!) + ?? panic("Cannot borrow FungibleToken contract for WETH") + let wethVaultData = wethContract.resolveContractView( + resourceType: wethVaultCompType, + viewType: Type() + ) as? FungibleTokenMetadataViews.FTVaultData + ?? panic("Cannot resolve FTVaultData for WETH") + + let wethProvider = signer.storage.borrow( + from: wethVaultData.storagePath + ) ?? panic("No WETH vault in signer's storage at ".concat(wethVaultData.storagePath.toString())) + + let inVault <- wethProvider.withdraw(amount: wethAmount) + + // Swap WETH → WBTC (bridges to EVM, swaps, bridges back to Cadence). + let outVault <- swapper.swap(quote: nil, inVault: <-inVault) + log("Provisioned ".concat(outVault.balance.toString()).concat(" WBTC from ".concat(wethAmount.toString()).concat(" WETH"))) + + // Set up WBTC vault in signer's storage if missing. + let wbtcVaultCompType = CompositeType(wbtcType.identifier) + ?? panic("Cannot construct CompositeType for WBTC: ".concat(wbtcType.identifier)) + let wbtcContract = getAccount(wbtcVaultCompType.address!).contracts.borrow<&{FungibleToken}>(name: wbtcVaultCompType.contractName!) + ?? panic("Cannot borrow FungibleToken contract for WBTC") + let wbtcVaultData = wbtcContract.resolveContractView( + resourceType: wbtcVaultCompType, + viewType: Type() + ) as? FungibleTokenMetadataViews.FTVaultData + ?? panic("Cannot resolve FTVaultData for WBTC") + + if signer.storage.borrow<&{FungibleToken.Vault}>(from: wbtcVaultData.storagePath) == nil { + signer.storage.save(<-wbtcVaultData.createEmptyVault(), to: wbtcVaultData.storagePath) + signer.capabilities.unpublish(wbtcVaultData.receiverPath) + signer.capabilities.unpublish(wbtcVaultData.metadataPath) + let receiverCap = signer.capabilities.storage.issue<&{FungibleToken.Vault}>(wbtcVaultData.storagePath) + let metadataCap = signer.capabilities.storage.issue<&{FungibleToken.Vault}>(wbtcVaultData.storagePath) + signer.capabilities.publish(receiverCap, at: wbtcVaultData.receiverPath) + signer.capabilities.publish(metadataCap, at: wbtcVaultData.metadataPath) + } + + let receiver = signer.storage.borrow<&{FungibleToken.Receiver}>(from: wbtcVaultData.storagePath) + ?? panic("Cannot borrow WBTC vault receiver") + receiver.deposit(from: <-outVault) + } +} diff --git a/cadence/transactions/flow-yield-vaults/admin/recreate_composer_issuer.cdc b/cadence/transactions/flow-yield-vaults/admin/recreate_composer_issuer.cdc new file mode 100644 index 00000000..8ac3bbe5 --- /dev/null +++ b/cadence/transactions/flow-yield-vaults/admin/recreate_composer_issuer.cdc @@ -0,0 +1,23 @@ +import "FlowYieldVaultsStrategiesV2" + +/// Admin transaction to recreate the StrategyComposerIssuer resource at IssuerStoragePath. +/// +/// Use this if the issuer was accidentally destroyed or is missing from storage. +/// Initialises with the default config (MorphoERC4626StrategyComposer / FUSDEVStrategy skeleton) +/// — run upsert_strategy_config / upsert_more_erc4626_config afterwards to repopulate configs. +/// +/// Must be signed by the account that deployed FlowYieldVaultsStrategiesV2. +transaction { + prepare(acct: auth(Storage) &Account) { + // Destroy any existing issuer so we can replace it cleanly + if acct.storage.type(at: FlowYieldVaultsStrategiesV2.IssuerStoragePath) != nil { + let old <- acct.storage.load<@FlowYieldVaultsStrategiesV2.StrategyComposerIssuer>( + from: FlowYieldVaultsStrategiesV2.IssuerStoragePath + ) + destroy old + } + + let issuer <- FlowYieldVaultsStrategiesV2.createIssuer() + acct.storage.save(<-issuer, to: FlowYieldVaultsStrategiesV2.IssuerStoragePath) + } +} 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..d906030d --- /dev/null +++ b/cadence/transactions/flow-yield-vaults/admin/upsert_more_erc4626_config.cdc @@ -0,0 +1,56 @@ +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] +) { + prepare(acct: auth(Storage) &Account) { + let strategyType = CompositeType(strategyTypeIdentifier) + ?? panic("Invalid strategyTypeIdentifier \(strategyTypeIdentifier)") + let tokenType = CompositeType(tokenTypeIdentifier) + ?? panic("Invalid tokenTypeIdentifier \(tokenTypeIdentifier)") + + let 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 + } + + issuer.addOrUpdateMoreERC4626CollateralConfig( + strategyType: strategyType, + collateralVaultType: tokenType, + yieldTokenEVMAddress: EVM.addressFromString(yieldTokenEVMAddress), + yieldToUnderlyingAddressPath: toEVM(yieldToUnderlyingPath), + yieldToUnderlyingFeePath: yieldToUnderlyingFees, + debtToCollateralAddressPath: toEVM(debtToCollateralPath), + debtToCollateralFeePath: debtToCollateralFees + ) + } +} diff --git a/flow.json b/flow.json index 9bc8b02b..9c9dbc33 100644 --- a/flow.json +++ b/flow.json @@ -1245,4 +1245,4 @@ ] } } -} \ No newline at end of file +} From d994f0ffc78b6cb64dabcea31328b032f9a96efd Mon Sep 17 00:00:00 2001 From: Alex <12097569+nialexsan@users.noreply.github.com> Date: Mon, 9 Mar 2026 10:45:37 -0400 Subject: [PATCH 09/27] generate negative tests --- ...wYieldVaultsStrategiesV2_syWFLOWv_test.cdc | 41 ++++++++++++++++ .../transactions/deposit_wrong_token.cdc | 48 +++++++++++++++++++ 2 files changed, 89 insertions(+) create mode 100644 cadence/tests/transactions/deposit_wrong_token.cdc diff --git a/cadence/tests/FlowYieldVaultsStrategiesV2_syWFLOWv_test.cdc b/cadence/tests/FlowYieldVaultsStrategiesV2_syWFLOWv_test.cdc index 8dc9e022..d901cec2 100644 --- a/cadence/tests/FlowYieldVaultsStrategiesV2_syWFLOWv_test.cdc +++ b/cadence/tests/FlowYieldVaultsStrategiesV2_syWFLOWv_test.cdc @@ -513,3 +513,44 @@ access(all) fun testCloseSyWFLOWvYieldVault_WETH() { Test.assert(vaultBalAfter.returnValue == nil, message: "WETH vault should no longer exist after close") log("WETH yield vault closed successfully") } + +/* ========================================================= + 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: ".concat(freshWethVaultID.toString()).concat(" — now attempting to deposit WBTC into it...")) + + // Attempt to deposit WBTC (wrong type) into the WETH vault — must fail + let depositResult = _executeTransactionFile( + "transactions/deposit_wrong_token.cdc", + [freshWethVaultID, wbtcVaultIdentifier, 0.00001], + [wethUser] + ) + Test.expect(depositResult, Test.beFailed()) + log("Correctly rejected wrong-token deposit (WBTC into WETH vault)") +} diff --git a/cadence/tests/transactions/deposit_wrong_token.cdc b/cadence/tests/transactions/deposit_wrong_token.cdc new file mode 100644 index 00000000..eacbe0a4 --- /dev/null +++ b/cadence/tests/transactions/deposit_wrong_token.cdc @@ -0,0 +1,48 @@ +import "FungibleToken" +import "FungibleTokenMetadataViews" + +import "FlowYieldVaults" +import "FlowYieldVaultsClosedBeta" + +/// Test-only transaction: attempts to deposit a token of the wrong type into an existing YieldVault. +/// The strategy's pre-condition should reject the mismatched vault type and cause this to fail. +/// +/// @param vaultID: The YieldVault to deposit into +/// @param wrongTokenTypeIdentifier: Type identifier of the wrong token to deposit +/// @param amount: Amount to withdraw from the signer's storage and attempt to deposit +/// +transaction(vaultID: UInt64, wrongTokenTypeIdentifier: String, amount: UFix64) { + let manager: &FlowYieldVaults.YieldVaultManager + let depositVault: @{FungibleToken.Vault} + let betaRef: auth(FlowYieldVaultsClosedBeta.Beta) &FlowYieldVaultsClosedBeta.BetaBadge + + prepare(signer: auth(BorrowValue, CopyValue) &Account) { + let betaCap = signer.storage.copy>( + from: FlowYieldVaultsClosedBeta.UserBetaCapStoragePath + ) ?? panic("Signer does not have a BetaBadge") + self.betaRef = betaCap.borrow() ?? panic("BetaBadge capability is invalid") + + self.manager = signer.storage.borrow<&FlowYieldVaults.YieldVaultManager>( + from: FlowYieldVaults.YieldVaultManagerStoragePath + ) ?? panic("Signer does not have a YieldVaultManager") + + let wrongType = CompositeType(wrongTokenTypeIdentifier) + ?? panic("Invalid type identifier \(wrongTokenTypeIdentifier)") + let tokenContract = getAccount(wrongType.address!).contracts.borrow<&{FungibleToken}>(name: wrongType.contractName!) + ?? panic("Type \(wrongTokenTypeIdentifier) is not a FungibleToken contract") + let vaultData = tokenContract.resolveContractView( + resourceType: wrongType, + viewType: Type() + ) as? FungibleTokenMetadataViews.FTVaultData + ?? panic("No FTVaultData for type \(wrongTokenTypeIdentifier)") + let sourceVault = signer.storage.borrow( + from: vaultData.storagePath + ) ?? panic("Signer has no vault of type \(wrongTokenTypeIdentifier) at path \(vaultData.storagePath)") + + self.depositVault <- sourceVault.withdraw(amount: amount) + } + + execute { + self.manager.depositToYieldVault(betaRef: self.betaRef, vaultID, from: <-self.depositVault) + } +} From 8e9ca3682e8e11229568c4d1bddbd0168ffb427b Mon Sep 17 00:00:00 2001 From: Alex <12097569+nialexsan@users.noreply.github.com> Date: Mon, 9 Mar 2026 14:05:09 -0400 Subject: [PATCH 10/27] generate FUSDEV test --- .../contracts/FlowYieldVaultsStrategiesV2.cdc | 218 ++++++- ...lowYieldVaultsStrategiesV2_FUSDEV_test.cdc | 571 ++++++++++++++++++ .../admin/upsert_strategy_config.cdc | 7 +- 3 files changed, 763 insertions(+), 33 deletions(-) create mode 100644 cadence/tests/FlowYieldVaultsStrategiesV2_FUSDEV_test.cdc diff --git a/cadence/contracts/FlowYieldVaultsStrategiesV2.cdc b/cadence/contracts/FlowYieldVaultsStrategiesV2.cdc index a29ab62c..cb4c4e58 100644 --- a/cadence/contracts/FlowYieldVaultsStrategiesV2.cdc +++ b/cadence/contracts/FlowYieldVaultsStrategiesV2.cdc @@ -53,6 +53,73 @@ access(all) contract FlowYieldVaultsStrategiesV2 { /// Canonical StoragePath where the StrategyComposerIssuer should be stored access(all) let IssuerStoragePath: StoragePath + /// A Source that converts yield tokens to debt tokens by pulling ALL available yield + /// tokens from the wrapped source, rather than using quoteIn to limit the pull amount. + /// + /// This avoids ERC4626 rounding issues where quoteIn might underestimate required shares, + /// causing the swap to return less than the requested debt amount. By pulling everything + /// and swapping everything, the output is as large as the yield position allows. + /// + /// The caller is responsible for ensuring the yield tokens (after swapping) will cover the + /// required debt — e.g. by pre-depositing supplemental MOET to reduce the position's debt + /// before calling closePosition (see FUSDEVStrategy.closePosition step 6). + access(all) struct BufferedSwapSource : DeFiActions.Source { + access(self) let swapper: {DeFiActions.Swapper} + access(self) let source: {DeFiActions.Source} + access(contract) var uniqueID: DeFiActions.UniqueIdentifier? + + init( + swapper: {DeFiActions.Swapper}, + source: {DeFiActions.Source}, + uniqueID: DeFiActions.UniqueIdentifier? + ) { + pre { + source.getSourceType() == swapper.inType(): + "source type != swapper inType" + } + self.swapper = swapper + self.source = source + self.uniqueID = uniqueID + } + + access(all) fun getComponentInfo(): DeFiActions.ComponentInfo { + return DeFiActions.ComponentInfo( + type: self.getType(), + id: self.id(), + innerComponents: [ + self.swapper.getComponentInfo(), + self.source.getComponentInfo() + ] + ) + } + access(contract) view fun copyID(): DeFiActions.UniqueIdentifier? { return self.uniqueID } + access(contract) fun setID(_ id: DeFiActions.UniqueIdentifier?) { self.uniqueID = id } + access(all) view fun getSourceType(): Type { return self.swapper.outType() } + access(all) fun minimumAvailable(): UFix64 { + let avail = self.source.minimumAvailable() + if avail == 0.0 { return 0.0 } + return self.swapper.quoteOut(forProvided: avail, reverse: false).outAmount + } + /// Pulls ALL available yield tokens from the source and swaps them to the debt token. + /// Ignores quoteIn — avoids ERC4626 rounding underestimates that would leave us short. + access(FungibleToken.Withdraw) fun withdrawAvailable(maxAmount: UFix64): @{FungibleToken.Vault} { + if maxAmount == 0.0 { + return <- DeFiActionsUtils.getEmptyVault(self.getSourceType()) + } + let availableIn = self.source.minimumAvailable() + if availableIn == 0.0 { + return <- DeFiActionsUtils.getEmptyVault(self.getSourceType()) + } + // Pull ALL available yield tokens (not quoteIn-limited) + let sourceLiquidity <- self.source.withdrawAvailable(maxAmount: availableIn) + if sourceLiquidity.balance == 0.0 { + Burner.burn(<-sourceLiquidity) + return <- DeFiActionsUtils.getEmptyVault(self.getSourceType()) + } + return <- self.swapper.swap(quote: nil, inVault: <-sourceLiquidity) + } + } + access(all) struct CollateralConfig { access(all) let yieldTokenEVMAddress: EVM.EVMAddress access(all) let yieldToCollateralUniV3AddressPath: [EVM.EVMAddress] @@ -228,54 +295,86 @@ access(all) contract FlowYieldVaultsStrategiesV2 { let yieldToMoetSwapper = FlowYieldVaultsStrategiesV2._getYieldToMoetSwapper(self.uniqueID!.id) ?? panic("No yield→MOET swapper found for strategy \(self.id()!)") - // Step 6: Create a SwapSource that converts yield tokens to MOET when pulled by closePosition. - // The pool will call source.withdrawAvailable(maxAmount: debtAmount) which internally uses - // quoteIn(forDesired: debtAmount) to compute the exact yield token input needed. - let moetSource = SwapConnectors.SwapSource( + // Step 6: Pre-supplement from collateral if yield is insufficient to cover the full debt. + // + // The FUSDEV close path has a structural ~0.02% round-trip fee loss: + // Open: MOET → PYUSD0 (UniV3 0.01%) → FUSDEV (ERC4626, free) + // Close: FUSDEV → PYUSD0 (ERC4626, free) → MOET (UniV3 0.01%) + // In production, accrued yield more than covers this; with no accrued yield (e.g. in + // tests, immediate open+close), the yield tokens convert back to slightly less MOET + // than was borrowed. We handle this by pre-pulling a tiny amount of collateral from + // self.source, swapping it to MOET, and depositing it into the position to reduce the + // outstanding debt — BEFORE calling position.closePosition. + // + // This MUST be done before closePosition because the position is locked during close: + // any attempt to pull from self.source inside a repaymentSource.withdrawAvailable call + // would trigger "Reentrancy: position X is locked". + let yieldAvail = yieldTokenSource.minimumAvailable() + let expectedMOET = yieldAvail > 0.0 + ? yieldToMoetSwapper.quoteOut(forProvided: yieldAvail, reverse: false).outAmount + : 0.0 + if expectedMOET < totalDebtAmount { + if let collateralToMoetSwapper = FlowYieldVaultsStrategiesV2._getCollateralToDebtSwapper(self.uniqueID!.id) { + let shortfall = totalDebtAmount - expectedMOET + // Add 1% buffer to account for swap slippage/rounding in the collateral→MOET leg + let buffered = shortfall + shortfall / 100.0 + let quote = collateralToMoetSwapper.quoteIn(forDesired: buffered, reverse: false) + if quote.inAmount > 0.0 { + let extraCollateral <- self.source.withdrawAvailable(maxAmount: quote.inAmount) + if extraCollateral.balance > 0.0 { + let extraMOET <- collateralToMoetSwapper.swap(quote: quote, inVault: <-extraCollateral) + if extraMOET.balance > 0.0 { + // Deposit MOET to reduce position debt before close + self.position.deposit(from: <-extraMOET) + } else { + Burner.burn(<-extraMOET) + } + } else { + Burner.burn(<-extraCollateral) + } + } + } + } + + // Step 7: Create a BufferedSwapSource that converts ALL yield tokens → MOET. + // Pulling all (not quoteIn-limited) avoids ERC4626 rounding underestimates. + // After the pre-supplement above, the remaining debt is covered by the yield tokens. + let moetSource = FlowYieldVaultsStrategiesV2.BufferedSwapSource( swapper: yieldToMoetSwapper, source: yieldTokenSource, uniqueID: self.copyID() ) - // Step 7: Close position - pool pulls exactly the debt amount from moetSource + // Step 8: Close position - pool pulls up to the (now pre-reduced) debt from moetSource let resultVaults <- self.position.closePosition(repaymentSources: [moetSource]) // With one collateral type and one debt type, the pool returns at most two vaults: // the collateral vault and optionally a MOET overpayment dust vault. - assert( - resultVaults.length >= 1 && resultVaults.length <= 2, - message: "Expected 1 or 2 vaults from closePosition, got \(resultVaults.length)" - ) - - var collateralVault <- resultVaults.removeFirst() - assert( - collateralVault.getType() == collateralType, - message: "First vault returned from closePosition must be collateral (\(collateralType.identifier)), got \(collateralVault.getType().identifier)" - ) - - // Handle any overpayment dust (MOET) returned as the second vault. - // nil means no swapper configured (old positions) — dust will be destroyed. + // closePosition returns vaults in dict-iteration order (hash-based), so we cannot + // assume the collateral vault is first. Find it by type and convert any non-collateral + // vaults (MOET overpayment dust) back to collateral via the stored swapper. let debtToCollateralSwapper = FlowYieldVaultsStrategiesV2._getDebtToCollateralSwapper(self.uniqueID!.id) + var collateralVault <- DeFiActionsUtils.getEmptyVault(collateralType) while resultVaults.length > 0 { - let dustVault <- resultVaults.removeFirst() - if dustVault.balance > 0.0 { - if dustVault.getType() == collateralType { - collateralVault.deposit(from: <-dustVault) - } else if let swapper = debtToCollateralSwapper { + let v <- resultVaults.removeFirst() + if v.getType() == collateralType { + collateralVault.deposit(from: <-v) + } else if v.balance > 0.0 { + if let swapper = debtToCollateralSwapper { // Quote first — if dust is too small to route, destroy it - let quote = swapper.quoteOut(forProvided: dustVault.balance, reverse: false) + let quote = swapper.quoteOut(forProvided: v.balance, reverse: false) if quote.outAmount > 0.0 { - let swapped <- swapper.swap(quote: quote, inVault: <-dustVault) + let swapped <- swapper.swap(quote: quote, inVault: <-v) collateralVault.deposit(from: <-swapped) } else { - Burner.burn(<-dustVault) + Burner.burn(<-v) } } else { - Burner.burn(<-dustVault) + Burner.burn(<-v) } } else { - Burner.burn(<-dustVault) + Burner.burn(<-v) } } @@ -803,6 +902,16 @@ access(all) contract FlowYieldVaultsStrategiesV2 { // Store yield→MOET swapper for later access during closePosition FlowYieldVaultsStrategiesV2._setYieldToMoetSwapper(uniqueID.id, yieldToDebtSwapper) + // Store collateral→MOET swapper for pre-supplement in closePosition. + // Used to cover the ~0.02% round-trip fee shortfall when yield hasn't accrued. + let collateralToDebtSwapper = self._createCollateralToDebtSwapper( + collateralConfig: collateralConfig, + tokens: tokens, + collateralType: collateralType, + uniqueID: uniqueID + ) + FlowYieldVaultsStrategiesV2._setCollateralToDebtSwapper(uniqueID.id, collateralToDebtSwapper) + // Store MOET→collateral swapper for dust conversion in closePosition. // Chain: MOET → FUSDEV (debtToYieldSwapper) → collateral (yieldToCollateralSwapper) FlowYieldVaultsStrategiesV2._setDebtToCollateralSwapper( @@ -1019,6 +1128,43 @@ access(all) contract FlowYieldVaultsStrategiesV2 { uniqueID: uniqueID ) } + + /// Creates a Collateral → Debt (MOET) swapper using UniswapV3. + /// Path: collateral → underlying (PYUSD0) → MOET + /// + /// The fee for collateral→underlying is the last fee in yieldToCollateral (reversed), + /// and the fee for underlying→MOET is fixed at 100 (0.01%, matching yieldToDebtSwapper). + /// Stored and used by FUSDEVStrategy.closePosition to pre-reduce position debt from + /// collateral when yield tokens alone cannot cover the full outstanding MOET debt. + /// + access(self) fun _createCollateralToDebtSwapper( + collateralConfig: FlowYieldVaultsStrategiesV2.CollateralConfig, + tokens: FlowYieldVaultsStrategiesV2.TokenBundle, + collateralType: Type, + uniqueID: DeFiActions.UniqueIdentifier + ): UniswapV3SwapConnectors.Swapper { + let yieldToCollPath = collateralConfig.yieldToCollateralUniV3AddressPath + let yieldToCollFees = collateralConfig.yieldToCollateralUniV3FeePath + + // collateral EVM address = last element of yieldToCollateral path + // underlying (PYUSD0) EVM address = second element of yieldToCollateral path + assert(yieldToCollPath.length >= 3, message: "yieldToCollateral path must have at least 3 elements") + let collateralEVMAddress = yieldToCollPath[yieldToCollPath.length - 1] + let underlyingEVMAddress = tokens.underlying4626AssetEVMAddress + + // fee[0] = collateral→underlying = last fee in yieldToCollateral (reversed) + // fee[1] = underlying→MOET = 100 (0.01%, matching _createYieldToDebtSwapper) + let collateralToUnderlyingFee = yieldToCollFees[yieldToCollFees.length - 1] + let underlyingToDebtFee: UInt32 = 100 + + return self._createUniV3Swapper( + tokenPath: [collateralEVMAddress, underlyingEVMAddress, tokens.moetTokenEVMAddress], + feePath: [collateralToUnderlyingFee, underlyingToDebtFee], + inVault: collateralType, + outVault: tokens.moetTokenType, + uniqueID: uniqueID + ) + } } /// This StrategyComposer builds strategies that borrow the ERC4626 vault's own underlying @@ -1528,6 +1674,22 @@ access(all) contract FlowYieldVaultsStrategiesV2 { FlowYieldVaultsStrategiesV2.config["debtToCollateralSwappers"] = partition } + // --- "collateralToDebtSwappers" partition --- + // Stores a collateral→debt (collateral→PYUSD0→MOET) UniV3 swapper per FUSDEVStrategy uniqueID. + // Used in FUSDEVStrategy.closePosition to pre-supplement the debt when yield tokens alone are + // insufficient (e.g. ~0.02% round-trip fee shortfall with no accrued yield). + + access(contract) view fun _getCollateralToDebtSwapper(_ id: UInt64): {DeFiActions.Swapper}? { + let partition = FlowYieldVaultsStrategiesV2.config["collateralToDebtSwappers"] as! {UInt64: {DeFiActions.Swapper}}? ?? {} + return partition[id] + } + + access(contract) fun _setCollateralToDebtSwapper(_ id: UInt64, _ swapper: {DeFiActions.Swapper}) { + var partition = FlowYieldVaultsStrategiesV2.config["collateralToDebtSwappers"] as! {UInt64: {DeFiActions.Swapper}}? ?? {} + partition[id] = swapper + FlowYieldVaultsStrategiesV2.config["collateralToDebtSwappers"] = partition + } + // --- "closedPositions" partition --- access(contract) view fun _isPositionClosed(_ uniqueID: DeFiActions.UniqueIdentifier?): Bool { diff --git a/cadence/tests/FlowYieldVaultsStrategiesV2_FUSDEV_test.cdc b/cadence/tests/FlowYieldVaultsStrategiesV2_FUSDEV_test.cdc new file mode 100644 index 00000000..be4f11d3 --- /dev/null +++ b/cadence/tests/FlowYieldVaultsStrategiesV2_FUSDEV_test.cdc @@ -0,0 +1,571 @@ +#test_fork(network: "mainnet", height: nil) + +import Test + +import "EVM" +import "FlowToken" +import "FlowYieldVaults" +import "FlowYieldVaultsClosedBeta" + +/// Fork test for FlowYieldVaultsStrategiesV2 FUSDEVStrategy. +/// +/// Tests the full YieldVault lifecycle (create, deposit, withdraw, close) for each supported +/// collateral type: WFLOW (FlowToken), WBTC, and WETH. +/// +/// PYUSD0 cannot be used as collateral — it is the FUSDEV vault's underlying asset. The +/// test setup intentionally omits a PYUSD0 collateral config so that negative tests can +/// assert the correct rejection. +/// +/// Strategy: +/// → FlowALP borrow MOET → swap MOET→PYUSD0 → ERC4626 deposit → FUSDEV (Morpho vault) +/// Close: FUSDEV → PYUSD0 (redeem) → MOET → repay FlowALP → returned to user +/// +/// Mainnet addresses: +/// - Admin (FlowYieldVaults deployer): 0xb1d63873c3cc9f79 +/// - WFLOW/PYUSD0 negative-test user: 0x443472749ebdaac8 (holds PYUSD0 and FLOW on mainnet) +/// - WBTC/WETH user: 0x68da18f20e98a7b6 (has ~12 WETH in EVM COA; WETH bridged + WBTC swapped in setup) +/// - UniV3 Factory: 0xca6d7Bb03334bBf135902e1d919a5feccb461632 +/// - UniV3 Router: 0xeEDC6Ff75e1b10B903D9013c358e446a73d35341 +/// - UniV3 Quoter: 0x370A8DF17742867a44e56223EC20D82092242C85 +/// - FUSDEV (Morpho ERC4626): 0xd069d989e2F44B70c65347d1853C0c67e10a9F8D +/// - PYUSD0: 0x99aF3EeA856556646C98c8B9b2548Fe815240750 +/// - WFLOW: 0xd3bF53DAC106A0290B0483EcBC89d40FcC961f3e +/// - WBTC: 0x717DAE2BaF7656BE9a9B01deE31d571a9d4c9579 (cbBTC; no WFLOW pool — use WETH as intermediate) +/// - WETH: 0x2F6F07CDcf3588944Bf4C42aC74ff24bF56e7590 + +// --- Accounts --- + +/// Mainnet admin account — deployer of FlowYieldVaults, FlowYieldVaultsClosedBeta, FlowYieldVaultsStrategiesV2 +access(all) let adminAccount = Test.getAccount(0xb1d63873c3cc9f79) + +/// WFLOW test user — holds FLOW (and PYUSD0) on mainnet. +/// Used for WFLOW lifecycle tests and for the negative PYUSD0 collateral test. +access(all) let flowUser = Test.getAccount(0x443472749ebdaac8) + +/// FlowToken contract account — used to provision FLOW to flowUser in setup. +access(all) let flowTokenAccount = Test.getAccount(0x1654653399040a61) + +/// WBTC/WETH holder — this account has ~12 WETH in its EVM COA on mainnet. +/// WETH is bridged to Cadence during setup(), and some WETH is then swapped → WBTC +/// via the UniV3 WETH/WBTC pool so that both collateral types can be tested. +/// COA EVM: 0x000000000000000000000002b87c966bc00bc2c4 +access(all) let wbtcUser = Test.getAccount(0x68da18f20e98a7b6) +access(all) let wethUser = Test.getAccount(0x68da18f20e98a7b6) + +// --- Strategy Config --- + +access(all) let fusdEvStrategyIdentifier = "A.b1d63873c3cc9f79.FlowYieldVaultsStrategiesV2.FUSDEVStrategy" +access(all) let composerIdentifier = "A.b1d63873c3cc9f79.FlowYieldVaultsStrategiesV2.MorphoERC4626StrategyComposer" +access(all) let issuerStoragePath: StoragePath = /storage/FlowYieldVaultsStrategyV2ComposerIssuer_0xb1d63873c3cc9f79 + +// --- Cadence Vault Type Identifiers --- + +/// FlowToken (WFLOW on EVM side) — used as WFLOW collateral +access(all) let flowVaultIdentifier = "A.1654653399040a61.FlowToken.Vault" +/// VM-bridged ERC-20 tokens +access(all) let wbtcVaultIdentifier = "A.1e4aa0b87d10b141.EVMVMBridgedToken_717dae2baf7656be9a9b01dee31d571a9d4c9579.Vault" +access(all) let wethVaultIdentifier = "A.1e4aa0b87d10b141.EVMVMBridgedToken_2f6f07cdcf3588944bf4c42ac74ff24bf56e7590.Vault" +access(all) let pyusd0VaultIdentifier = "A.1e4aa0b87d10b141.EVMVMBridgedToken_99af3eea856556646c98c8b9b2548fe815240750.Vault" + +// --- EVM Addresses --- + +access(all) let fusdEvEVMAddress = "0xd069d989e2F44B70c65347d1853C0c67e10a9F8D" +access(all) let pyusd0EVMAddress = "0x99aF3EeA856556646C98c8B9b2548Fe815240750" +access(all) let wflowEVMAddress = "0xd3bF53DAC106A0290B0483EcBC89d40FcC961f3e" +access(all) let wbtcEVMAddress = "0x717DAE2BaF7656BE9a9B01deE31d571a9d4c9579" +access(all) let wethEVMAddress = "0x2F6F07CDcf3588944Bf4C42aC74ff24bF56e7590" + +// --- Test State (vault IDs set during create tests, read by subsequent tests) --- + +access(all) var flowVaultID: UInt64 = 0 +access(all) var wbtcVaultID: UInt64 = 0 +access(all) var wethVaultID: UInt64 = 0 + +/* --- Helpers --- */ + +access(all) +fun _executeScript(_ path: String, _ args: [AnyStruct]): Test.ScriptResult { + return Test.executeScript(Test.readFile(path), args) +} + +access(all) +fun _executeTransactionFile(_ path: String, _ args: [AnyStruct], _ signers: [Test.TestAccount]): Test.TransactionResult { + let txn = Test.Transaction( + code: Test.readFile(path), + authorizers: signers.map(fun (s: Test.TestAccount): Address { return s.address }), + signers: signers, + arguments: args + ) + return Test.executeTransaction(txn) +} + +access(all) +fun equalAmounts(a: UFix64, b: UFix64, tolerance: UFix64): Bool { + if a > b { return a - b <= tolerance } + return b - a <= tolerance +} + +/// Returns the most-recently-created YieldVault ID for the given account. +access(all) +fun _latestVaultID(_ user: Test.TestAccount): UInt64 { + let r = _executeScript("../scripts/flow-yield-vaults/get_yield_vault_ids.cdc", [user.address]) + Test.expect(r, Test.beSucceeded()) + let ids = r.returnValue! as! [UInt64]? + Test.assert(ids != nil && ids!.length > 0, message: "Expected at least one yield vault for ".concat(user.address.toString())) + return ids![ids!.length - 1] +} + +/* --- Setup --- */ + +access(all) fun setup() { + log("==== FlowYieldVaultsStrategiesV2 FUSDEV Fork Test Setup ====") + + log("Deploying EVMAmountUtils...") + var err = Test.deployContract( + name: "EVMAmountUtils", + path: "../../lib/FlowALP/FlowActions/cadence/contracts/connectors/evm/EVMAmountUtils.cdc", + arguments: [] + ) + Test.expect(err, Test.beNil()) + + log("Deploying UniswapV3SwapConnectors...") + err = Test.deployContract( + name: "UniswapV3SwapConnectors", + path: "../../lib/FlowALP/FlowActions/cadence/contracts/connectors/evm/UniswapV3SwapConnectors.cdc", + arguments: [] + ) + Test.expect(err, Test.beNil()) + + log("Deploying ERC4626Utils...") + err = Test.deployContract( + name: "ERC4626Utils", + path: "../../lib/FlowALP/FlowActions/cadence/contracts/utils/ERC4626Utils.cdc", + arguments: [] + ) + Test.expect(err, Test.beNil()) + + log("Deploying ERC4626SwapConnectors...") + err = Test.deployContract( + name: "ERC4626SwapConnectors", + path: "../../lib/FlowALP/FlowActions/cadence/contracts/connectors/evm/ERC4626SwapConnectors.cdc", + arguments: [] + ) + Test.expect(err, Test.beNil()) + + // MorphoERC4626SinkConnectors must come before MorphoERC4626SwapConnectors (it imports it). + log("Deploying MorphoERC4626SinkConnectors...") + err = Test.deployContract( + name: "MorphoERC4626SinkConnectors", + path: "../../lib/FlowALP/FlowActions/cadence/contracts/connectors/evm/morpho/MorphoERC4626SinkConnectors.cdc", + arguments: [] + ) + Test.expect(err, Test.beNil()) + + log("Deploying MorphoERC4626SwapConnectors...") + err = Test.deployContract( + name: "MorphoERC4626SwapConnectors", + path: "../../lib/FlowALP/FlowActions/cadence/contracts/connectors/evm/morpho/MorphoERC4626SwapConnectors.cdc", + arguments: [] + ) + Test.expect(err, Test.beNil()) + + log("Deploying FlowYieldVaults...") + err = Test.deployContract( + name: "FlowYieldVaults", + path: "../../cadence/contracts/FlowYieldVaults.cdc", + arguments: [] + ) + Test.expect(err, Test.beNil()) + + log("Deploying ERC4626PriceOracles...") + err = Test.deployContract( + name: "ERC4626PriceOracles", + path: "../../lib/FlowALP/FlowActions/cadence/contracts/connectors/evm/ERC4626PriceOracles.cdc", + arguments: [] + ) + Test.expect(err, Test.beNil()) + + log("Deploying FlowALPv0...") + err = Test.deployContract( + name: "FlowALPv0", + path: "../../lib/FlowALP/cadence/contracts/FlowALPv0.cdc", + arguments: [] + ) + Test.expect(err, Test.beNil()) + + log("Deploying FlowYieldVaultsAutoBalancers...") + err = Test.deployContract( + name: "FlowYieldVaultsAutoBalancers", + path: "../../cadence/contracts/FlowYieldVaultsAutoBalancers.cdc", + arguments: [] + ) + Test.expect(err, Test.beNil()) + + log("Deploying FlowYieldVaultsStrategiesV2...") + err = Test.deployContract( + name: "FlowYieldVaultsStrategiesV2", + path: "../../cadence/contracts/FlowYieldVaultsStrategiesV2.cdc", + arguments: [ + "0xca6d7Bb03334bBf135902e1d919a5feccb461632", + "0xeEDC6Ff75e1b10B903D9013c358e446a73d35341", + "0x370A8DF17742867a44e56223EC20D82092242C85" + ] + ) + Test.expect(err, Test.beNil()) + + // Recreate the StrategyComposerIssuer (deleted from mainnet storage on contract redeploy). + log("Recreating StrategyComposerIssuer...") + var result = _executeTransactionFile( + "../transactions/flow-yield-vaults/admin/recreate_composer_issuer.cdc", + [], + [adminAccount] + ) + Test.expect(result, Test.beSucceeded()) + + // Configure UniV3 paths for FUSDEVStrategy. + // Closing direction: FUSDEV → PYUSD0 (Morpho redeem, fee 100) → (UniV3 swap, fee 3000). + // PYUSD0 is intentionally NOT configured as collateral — it is the underlying asset. + + log("Configuring FUSDEVStrategy + WFLOW (FUSDEV→PYUSD0→WFLOW fees 100/3000)...") + result = _executeTransactionFile( + "../transactions/flow-yield-vaults/admin/upsert_strategy_config.cdc", + [ + fusdEvStrategyIdentifier, + flowVaultIdentifier, + fusdEvEVMAddress, + [fusdEvEVMAddress, pyusd0EVMAddress, wflowEVMAddress], + [100 as UInt32, 3000 as UInt32] + ], + [adminAccount] + ) + Test.expect(result, Test.beSucceeded()) + + // No WFLOW/WBTC pool on Flow EVM — PYUSD0 is the intermediate for both legs. + log("Configuring FUSDEVStrategy + WBTC (FUSDEV→PYUSD0→WBTC fees 100/3000)...") + result = _executeTransactionFile( + "../transactions/flow-yield-vaults/admin/upsert_strategy_config.cdc", + [ + fusdEvStrategyIdentifier, + wbtcVaultIdentifier, + fusdEvEVMAddress, + [fusdEvEVMAddress, pyusd0EVMAddress, wbtcEVMAddress], + [100 as UInt32, 3000 as UInt32] + ], + [adminAccount] + ) + Test.expect(result, Test.beSucceeded()) + + log("Configuring FUSDEVStrategy + WETH (FUSDEV→PYUSD0→WETH fees 100/3000)...") + result = _executeTransactionFile( + "../transactions/flow-yield-vaults/admin/upsert_strategy_config.cdc", + [ + fusdEvStrategyIdentifier, + wethVaultIdentifier, + fusdEvEVMAddress, + [fusdEvEVMAddress, pyusd0EVMAddress, wethEVMAddress], + [100 as UInt32, 3000 as UInt32] + ], + [adminAccount] + ) + Test.expect(result, Test.beSucceeded()) + + // Register FUSDEVStrategy in the FlowYieldVaults StrategyFactory + log("Registering FUSDEVStrategy in FlowYieldVaults factory...") + result = _executeTransactionFile( + "../transactions/flow-yield-vaults/admin/add_strategy_composer.cdc", + [fusdEvStrategyIdentifier, composerIdentifier, issuerStoragePath], + [adminAccount] + ) + Test.expect(result, Test.beSucceeded()) + + // Grant beta access to all user accounts + log("Granting beta access to WFLOW/PYUSD0 user...") + result = _executeTransactionFile( + "../transactions/flow-yield-vaults/admin/grant_beta.cdc", + [], + [adminAccount, flowUser] + ) + Test.expect(result, Test.beSucceeded()) + + log("Granting beta access to WBTC/WETH user (0x68da18f20e98a7b6)...") + result = _executeTransactionFile( + "../transactions/flow-yield-vaults/admin/grant_beta.cdc", + [], + [adminAccount, wbtcUser] + ) + Test.expect(result, Test.beSucceeded()) + + // Provision extra FLOW to flowUser so that testDepositToFUSDEVYieldVault_WFLOW has enough balance. + // flowUser starts with ~11 FLOW; the create uses 10.0, leaving ~1 FLOW — not enough for a 5.0 deposit. + log("Provisioning 20.0 FLOW to WFLOW user from FlowToken contract account...") + result = _executeTransactionFile( + "../transactions/flow-token/transfer_flow.cdc", + [flowUser.address, 20.0], + [flowTokenAccount] + ) + Test.expect(result, Test.beSucceeded()) + + // Provision WETH and WBTC for the WBTC/WETH user. + // The COA at 0x000000000000000000000002b87c966bc00bc2c4 holds ~12 WETH on mainnet. + log("Bridging 2 WETH from COA to Cadence and swapping 0.1 WETH → WBTC for WBTC/WETH user...") + + // Bridge 2 WETH (2_000_000_000_000_000_000 at 18 decimals) from COA to Cadence. + let bridgeResult = _executeTransactionFile( + "../../lib/FlowALP/FlowActions/cadence/tests/transactions/bridge/bridge_tokens_from_evm.cdc", + [wethVaultIdentifier, 2000000000000000000 as UInt256], + [wbtcUser] + ) + Test.expect(bridgeResult, Test.beSucceeded()) + + // Swap 0.1 WETH → WBTC via UniV3 WETH/WBTC pool (fee 3000). + let swapResult = _executeTransactionFile( + "transactions/provision_wbtc_from_weth.cdc", + [ + "0xca6d7Bb03334bBf135902e1d919a5feccb461632", // UniV3 factory + "0xeEDC6Ff75e1b10B903D9013c358e446a73d35341", // UniV3 router + "0x370A8DF17742867a44e56223EC20D82092242C85", // UniV3 quoter + wethEVMAddress, + wbtcEVMAddress, + 3000 as UInt32, + 0.1 as UFix64 + ], + [wbtcUser] + ) + Test.expect(swapResult, Test.beSucceeded()) + + log("==== Setup Complete ====") +} + +/* ========================================================= + WFLOW (FlowToken) collateral lifecycle + ========================================================= */ + +access(all) fun testCreateFUSDEVYieldVault_WFLOW() { + log("Creating FUSDEVStrategy yield vault with 10.0 FLOW...") + let result = _executeTransactionFile( + "../transactions/flow-yield-vaults/create_yield_vault.cdc", + [fusdEvStrategyIdentifier, flowVaultIdentifier, 10.0], + [flowUser] + ) + Test.expect(result, Test.beSucceeded()) + + flowVaultID = _latestVaultID(flowUser) + log("Created WFLOW vault ID: ".concat(flowVaultID.toString())) + + let bal = _executeScript("../scripts/flow-yield-vaults/get_yield_vault_balance.cdc", [flowUser.address, flowVaultID]) + Test.expect(bal, Test.beSucceeded()) + let balance = bal.returnValue! as! UFix64? + Test.assert(balance != nil && balance! > 0.0, message: "Expected positive balance after create (WFLOW)") + log("WFLOW vault balance after create: ".concat(balance!.toString())) +} + +access(all) fun testDepositToFUSDEVYieldVault_WFLOW() { + let before = (_executeScript("../scripts/flow-yield-vaults/get_yield_vault_balance.cdc", [flowUser.address, flowVaultID]).returnValue! as! UFix64?) ?? 0.0 + let depositAmount: UFix64 = 5.0 + log("Depositing 5.0 FLOW to vault ".concat(flowVaultID.toString()).concat("...")) + Test.expect( + _executeTransactionFile("../transactions/flow-yield-vaults/deposit_to_yield_vault.cdc", [flowVaultID, depositAmount], [flowUser]), + Test.beSucceeded() + ) + let after = (_executeScript("../scripts/flow-yield-vaults/get_yield_vault_balance.cdc", [flowUser.address, flowVaultID]).returnValue! as! UFix64?)! + Test.assert(equalAmounts(a: after, b: before + depositAmount, tolerance: 0.1), + message: "WFLOW deposit: expected ~".concat((before + depositAmount).toString()).concat(", got ").concat(after.toString())) + log("WFLOW vault balance after deposit: ".concat(after.toString())) +} + +access(all) fun testWithdrawFromFUSDEVYieldVault_WFLOW() { + let before = (_executeScript("../scripts/flow-yield-vaults/get_yield_vault_balance.cdc", [flowUser.address, flowVaultID]).returnValue! as! UFix64?) ?? 0.0 + let withdrawAmount: UFix64 = 3.0 + log("Withdrawing 3.0 FLOW from vault ".concat(flowVaultID.toString()).concat("...")) + Test.expect( + _executeTransactionFile("../transactions/flow-yield-vaults/withdraw_from_yield_vault.cdc", [flowVaultID, withdrawAmount], [flowUser]), + Test.beSucceeded() + ) + let after = (_executeScript("../scripts/flow-yield-vaults/get_yield_vault_balance.cdc", [flowUser.address, flowVaultID]).returnValue! as! UFix64?)! + Test.assert(equalAmounts(a: after, b: before - withdrawAmount, tolerance: 0.1), + message: "WFLOW withdraw: expected ~".concat((before - withdrawAmount).toString()).concat(", got ").concat(after.toString())) + log("WFLOW vault balance after withdrawal: ".concat(after.toString())) +} + +access(all) fun testCloseFUSDEVYieldVault_WFLOW() { + let vaultBalBefore = (_executeScript("../scripts/flow-yield-vaults/get_yield_vault_balance.cdc", [flowUser.address, flowVaultID]).returnValue! as! UFix64?) ?? 0.0 + log("Closing WFLOW vault ".concat(flowVaultID.toString()).concat(" (balance: ").concat(vaultBalBefore.toString()).concat(")...")) + Test.expect( + _executeTransactionFile("../transactions/flow-yield-vaults/close_yield_vault.cdc", [flowVaultID], [flowUser]), + Test.beSucceeded() + ) + let vaultBalAfter = _executeScript("../scripts/flow-yield-vaults/get_yield_vault_balance.cdc", [flowUser.address, flowVaultID]) + Test.expect(vaultBalAfter, Test.beSucceeded()) + Test.assert(vaultBalAfter.returnValue == nil, message: "WFLOW vault should no longer exist after close") + log("WFLOW yield vault closed successfully") +} + +/* ========================================================= + WBTC collateral lifecycle + ========================================================= */ + +access(all) fun testCreateFUSDEVYieldVault_WBTC() { + log("Creating FUSDEVStrategy yield vault with 0.0001 WBTC...") + let result = _executeTransactionFile( + "../transactions/flow-yield-vaults/create_yield_vault.cdc", + [fusdEvStrategyIdentifier, wbtcVaultIdentifier, 0.0001], + [wbtcUser] + ) + Test.expect(result, Test.beSucceeded()) + + wbtcVaultID = _latestVaultID(wbtcUser) + log("Created WBTC vault ID: ".concat(wbtcVaultID.toString())) + + let bal = _executeScript("../scripts/flow-yield-vaults/get_yield_vault_balance.cdc", [wbtcUser.address, wbtcVaultID]) + Test.expect(bal, Test.beSucceeded()) + let balance = bal.returnValue! as! UFix64? + Test.assert(balance != nil && balance! > 0.0, message: "Expected positive balance after create (WBTC)") + log("WBTC vault balance after create: ".concat(balance!.toString())) +} + +access(all) fun testDepositToFUSDEVYieldVault_WBTC() { + let before = (_executeScript("../scripts/flow-yield-vaults/get_yield_vault_balance.cdc", [wbtcUser.address, wbtcVaultID]).returnValue! as! UFix64?) ?? 0.0 + let depositAmount: UFix64 = 0.00005 + log("Depositing 0.00005 WBTC to vault ".concat(wbtcVaultID.toString()).concat("...")) + Test.expect( + _executeTransactionFile("../transactions/flow-yield-vaults/deposit_to_yield_vault.cdc", [wbtcVaultID, depositAmount], [wbtcUser]), + Test.beSucceeded() + ) + let after = (_executeScript("../scripts/flow-yield-vaults/get_yield_vault_balance.cdc", [wbtcUser.address, wbtcVaultID]).returnValue! as! UFix64?)! + Test.assert(equalAmounts(a: after, b: before + depositAmount, tolerance: 0.000005), + message: "WBTC deposit: expected ~".concat((before + depositAmount).toString()).concat(", got ").concat(after.toString())) + log("WBTC vault balance after deposit: ".concat(after.toString())) +} + +access(all) fun testWithdrawFromFUSDEVYieldVault_WBTC() { + let before = (_executeScript("../scripts/flow-yield-vaults/get_yield_vault_balance.cdc", [wbtcUser.address, wbtcVaultID]).returnValue! as! UFix64?) ?? 0.0 + let withdrawAmount: UFix64 = 0.00003 + log("Withdrawing 0.00003 WBTC from vault ".concat(wbtcVaultID.toString()).concat("...")) + Test.expect( + _executeTransactionFile("../transactions/flow-yield-vaults/withdraw_from_yield_vault.cdc", [wbtcVaultID, withdrawAmount], [wbtcUser]), + Test.beSucceeded() + ) + let after = (_executeScript("../scripts/flow-yield-vaults/get_yield_vault_balance.cdc", [wbtcUser.address, wbtcVaultID]).returnValue! as! UFix64?)! + Test.assert(equalAmounts(a: after, b: before - withdrawAmount, tolerance: 0.000005), + message: "WBTC withdraw: expected ~".concat((before - withdrawAmount).toString()).concat(", got ").concat(after.toString())) + log("WBTC vault balance after withdrawal: ".concat(after.toString())) +} + +access(all) fun testCloseFUSDEVYieldVault_WBTC() { + let vaultBalBefore = (_executeScript("../scripts/flow-yield-vaults/get_yield_vault_balance.cdc", [wbtcUser.address, wbtcVaultID]).returnValue! as! UFix64?) ?? 0.0 + log("Closing WBTC vault ".concat(wbtcVaultID.toString()).concat(" (balance: ").concat(vaultBalBefore.toString()).concat(")...")) + Test.expect( + _executeTransactionFile("../transactions/flow-yield-vaults/close_yield_vault.cdc", [wbtcVaultID], [wbtcUser]), + Test.beSucceeded() + ) + let vaultBalAfter = _executeScript("../scripts/flow-yield-vaults/get_yield_vault_balance.cdc", [wbtcUser.address, wbtcVaultID]) + Test.expect(vaultBalAfter, Test.beSucceeded()) + Test.assert(vaultBalAfter.returnValue == nil, message: "WBTC vault should no longer exist after close") + log("WBTC yield vault closed successfully") +} + +/* ========================================================= + WETH collateral lifecycle + ========================================================= */ + +access(all) fun testCreateFUSDEVYieldVault_WETH() { + log("Creating FUSDEVStrategy yield vault with 0.001 WETH...") + let result = _executeTransactionFile( + "../transactions/flow-yield-vaults/create_yield_vault.cdc", + [fusdEvStrategyIdentifier, wethVaultIdentifier, 0.001], + [wethUser] + ) + Test.expect(result, Test.beSucceeded()) + + wethVaultID = _latestVaultID(wethUser) + log("Created WETH vault ID: ".concat(wethVaultID.toString())) + + let bal = _executeScript("../scripts/flow-yield-vaults/get_yield_vault_balance.cdc", [wethUser.address, wethVaultID]) + Test.expect(bal, Test.beSucceeded()) + let balance = bal.returnValue! as! UFix64? + Test.assert(balance != nil && balance! > 0.0, message: "Expected positive balance after create (WETH)") + log("WETH vault balance after create: ".concat(balance!.toString())) +} + +access(all) fun testDepositToFUSDEVYieldVault_WETH() { + let before = (_executeScript("../scripts/flow-yield-vaults/get_yield_vault_balance.cdc", [wethUser.address, wethVaultID]).returnValue! as! UFix64?) ?? 0.0 + let depositAmount: UFix64 = 0.0005 + log("Depositing 0.0005 WETH to vault ".concat(wethVaultID.toString()).concat("...")) + Test.expect( + _executeTransactionFile("../transactions/flow-yield-vaults/deposit_to_yield_vault.cdc", [wethVaultID, depositAmount], [wethUser]), + Test.beSucceeded() + ) + let after = (_executeScript("../scripts/flow-yield-vaults/get_yield_vault_balance.cdc", [wethUser.address, wethVaultID]).returnValue! as! UFix64?)! + Test.assert(equalAmounts(a: after, b: before + depositAmount, tolerance: 0.00005), + message: "WETH deposit: expected ~".concat((before + depositAmount).toString()).concat(", got ").concat(after.toString())) + log("WETH vault balance after deposit: ".concat(after.toString())) +} + +access(all) fun testWithdrawFromFUSDEVYieldVault_WETH() { + let before = (_executeScript("../scripts/flow-yield-vaults/get_yield_vault_balance.cdc", [wethUser.address, wethVaultID]).returnValue! as! UFix64?) ?? 0.0 + let withdrawAmount: UFix64 = 0.0003 + log("Withdrawing 0.0003 WETH from vault ".concat(wethVaultID.toString()).concat("...")) + Test.expect( + _executeTransactionFile("../transactions/flow-yield-vaults/withdraw_from_yield_vault.cdc", [wethVaultID, withdrawAmount], [wethUser]), + Test.beSucceeded() + ) + let after = (_executeScript("../scripts/flow-yield-vaults/get_yield_vault_balance.cdc", [wethUser.address, wethVaultID]).returnValue! as! UFix64?)! + Test.assert(equalAmounts(a: after, b: before - withdrawAmount, tolerance: 0.00005), + message: "WETH withdraw: expected ~".concat((before - withdrawAmount).toString()).concat(", got ").concat(after.toString())) + log("WETH vault balance after withdrawal: ".concat(after.toString())) +} + +access(all) fun testCloseFUSDEVYieldVault_WETH() { + let vaultBalBefore = (_executeScript("../scripts/flow-yield-vaults/get_yield_vault_balance.cdc", [wethUser.address, wethVaultID]).returnValue! as! UFix64?) ?? 0.0 + log("Closing WETH vault ".concat(wethVaultID.toString()).concat(" (balance: ").concat(vaultBalBefore.toString()).concat(")...")) + Test.expect( + _executeTransactionFile("../transactions/flow-yield-vaults/close_yield_vault.cdc", [wethVaultID], [wethUser]), + Test.beSucceeded() + ) + let vaultBalAfter = _executeScript("../scripts/flow-yield-vaults/get_yield_vault_balance.cdc", [wethUser.address, wethVaultID]) + Test.expect(vaultBalAfter, Test.beSucceeded()) + Test.assert(vaultBalAfter.returnValue == nil, message: "WETH vault should no longer exist after close") + log("WETH yield vault closed successfully") +} + +/* ========================================================= + Negative tests + ========================================================= */ + +/// PYUSD0 is the underlying asset of FUSDEV — the strategy composer has no collateral config for +/// it, so attempting to create a vault with PYUSD0 as collateral must be rejected. +access(all) fun testCannotCreateYieldVaultWithPYUSD0AsCollateral() { + log("Attempting to create FUSDEVStrategy vault with PYUSD0 (underlying asset) as collateral — expecting failure...") + let result = _executeTransactionFile( + "../transactions/flow-yield-vaults/create_yield_vault.cdc", + [fusdEvStrategyIdentifier, pyusd0VaultIdentifier, 1.0], + [flowUser] + ) + Test.expect(result, Test.beFailed()) + log("Correctly rejected PYUSD0 as collateral") +} + +/// Depositing the wrong token type into an existing YieldVault must be rejected. +/// Here wethUser owns both WETH and WBTC (set up in setup()). +/// We create a fresh WETH vault, then attempt to deposit WBTC into it — the strategy +/// pre-condition should panic on the type mismatch. +access(all) fun testCannotDepositWrongTokenToYieldVault() { + log("Creating a fresh WETH vault for wrong-token deposit test...") + let createResult = _executeTransactionFile( + "../transactions/flow-yield-vaults/create_yield_vault.cdc", + [fusdEvStrategyIdentifier, wethVaultIdentifier, 0.001], + [wethUser] + ) + Test.expect(createResult, Test.beSucceeded()) + let freshWethVaultID = _latestVaultID(wethUser) + log("Created WETH vault ID: ".concat(freshWethVaultID.toString()).concat(" — now attempting to deposit WBTC into it...")) + + // Attempt to deposit WBTC (wrong type) into the WETH vault — must fail + let depositResult = _executeTransactionFile( + "transactions/deposit_wrong_token.cdc", + [freshWethVaultID, wbtcVaultIdentifier, 0.00001], + [wethUser] + ) + Test.expect(depositResult, Test.beFailed()) + log("Correctly rejected wrong-token deposit (WBTC into WETH vault)") +} diff --git a/cadence/transactions/flow-yield-vaults/admin/upsert_strategy_config.cdc b/cadence/transactions/flow-yield-vaults/admin/upsert_strategy_config.cdc index 792c7031..d36da60b 100644 --- a/cadence/transactions/flow-yield-vaults/admin/upsert_strategy_config.cdc +++ b/cadence/transactions/flow-yield-vaults/admin/upsert_strategy_config.cdc @@ -45,16 +45,13 @@ transaction( return out } - let composerType = Type<@FlowYieldVaultsStrategiesV2.MorphoERC4626StrategyComposer>() - if swapPath.length > 0 { - issuer.addOrUpdateCollateralConfig( - composer: composerType, + issuer.addOrUpdateMorphoCollateralConfig( strategyType: strategyType, collateralVaultType: tokenType, yieldTokenEVMAddress: yieldEVM, yieldToCollateralAddressPath: toEVM(swapPath), - yieldToCollateralFeePath: fees + yieldToCollateralFeePath: fees ) } } From 4599115104554f9c0a591c922c65ed232eeba0fd Mon Sep 17 00:00:00 2001 From: Alex <12097569+nialexsan@users.noreply.github.com> Date: Mon, 9 Mar 2026 14:13:26 -0400 Subject: [PATCH 11/27] remove todo --- cadence/contracts/FlowYieldVaultsStrategiesV2.cdc | 9 --------- 1 file changed, 9 deletions(-) diff --git a/cadence/contracts/FlowYieldVaultsStrategiesV2.cdc b/cadence/contracts/FlowYieldVaultsStrategiesV2.cdc index cb4c4e58..65993226 100644 --- a/cadence/contracts/FlowYieldVaultsStrategiesV2.cdc +++ b/cadence/contracts/FlowYieldVaultsStrategiesV2.cdc @@ -1057,15 +1057,6 @@ access(all) contract FlowYieldVaultsStrategiesV2 { ) } - /// @TODO - /// implement moet to collateral swapper - // access(self) fun _createMoetToCollateralSwapper( - // tokens: FlowYieldVaultsStrategiesV2.TokenBundle, - // uniqueID: DeFiActions.UniqueIdentifier - // ): SwapConnectors.MultiSwapper { - // // Direct MOET -> underlying via AMM - // } - access(self) fun _createYieldToCollateralSwapper( collateralConfig: FlowYieldVaultsStrategiesV2.CollateralConfig, yieldTokenEVMAddress: EVM.EVMAddress, From a35ba72d6260f0a55763534a66bd2a9d9b58c745 Mon Sep 17 00:00:00 2001 From: Alex <12097569+nialexsan@users.noreply.github.com> Date: Mon, 9 Mar 2026 18:58:53 -0400 Subject: [PATCH 12/27] fix mock strategy path --- cadence/contracts/mocks/MockStrategies.cdc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cadence/contracts/mocks/MockStrategies.cdc b/cadence/contracts/mocks/MockStrategies.cdc index 0b72ae89..530a3555 100644 --- a/cadence/contracts/mocks/MockStrategies.cdc +++ b/cadence/contracts/mocks/MockStrategies.cdc @@ -495,7 +495,7 @@ access(all) contract MockStrategies { } init() { - self.IssuerStoragePath = StoragePath(identifier: "MockStrategyComposerIssuer_\(self.account.address)")! + self.IssuerStoragePath = StoragePath(identifier: "MockStrategiesComposerIssuer_\(self.account.address)")! let initialCollateralType = Type<@FlowToken.Vault>() From b798562bd9818c28975192b4a9f7bf6fac358314 Mon Sep 17 00:00:00 2001 From: Alex <12097569+nialexsan@users.noreply.github.com> Date: Mon, 9 Mar 2026 21:43:55 -0400 Subject: [PATCH 13/27] update FlowALP ref --- lib/FlowALP | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/FlowALP b/lib/FlowALP index ee6fb772..02c6a92c 160000 --- a/lib/FlowALP +++ b/lib/FlowALP @@ -1 +1 @@ -Subproject commit ee6fb772aa24ab9867e0e28bacd8e9e1a0f1fe58 +Subproject commit 02c6a92cc4f6d1210e3db19c617db9916c3ad388 From 45dcb9b33e21512bd1a8ac3e6e87a2ba435767ee Mon Sep 17 00:00:00 2001 From: Alex <12097569+nialexsan@users.noreply.github.com> Date: Mon, 9 Mar 2026 22:01:09 -0400 Subject: [PATCH 14/27] remove redundant rebalance calls in tests --- cadence/tests/rebalance_scenario1_test.cdc | 2 -- cadence/tests/rebalance_scenario2_test.cdc | 1 - cadence/tests/rebalance_scenario3a_test.cdc | 1 - cadence/tests/rebalance_scenario3b_test.cdc | 1 - cadence/tests/rebalance_scenario3c_test.cdc | 1 - cadence/tests/rebalance_scenario3d_test.cdc | 1 - cadence/tests/rebalance_scenario4_test.cdc | 4 +--- cadence/tests/rebalance_yield_test.cdc | 1 - cadence/tests/tracer_strategy_test.cdc | 7 ++----- 9 files changed, 3 insertions(+), 16 deletions(-) diff --git a/cadence/tests/rebalance_scenario1_test.cdc b/cadence/tests/rebalance_scenario1_test.cdc index c604fb0e..267e9169 100644 --- a/cadence/tests/rebalance_scenario1_test.cdc +++ b/cadence/tests/rebalance_scenario1_test.cdc @@ -124,7 +124,6 @@ fun test_RebalanceYieldVaultScenario1() { log("[TEST] Initial yield vault balance: \(yieldVaultBalance ?? 0.0)") - rebalanceYieldVault(signer: flowYieldVaultsAccount, id: yieldVaultIDs![0], force: true, beFailed: false) rebalancePosition(signer: protocolAccount, pid: pid, force: true, beFailed: false) testSnapshot = getCurrentBlockHeight() @@ -147,7 +146,6 @@ fun test_RebalanceYieldVaultScenario1() { let yieldTokensBefore = getAutoBalancerBalance(id: yieldVaultIDs![0]) ?? 0.0 let currentValueBefore = getAutoBalancerCurrentValue(id: yieldVaultIDs![0]) ?? 0.0 - rebalanceYieldVault(signer: flowYieldVaultsAccount, id: yieldVaultIDs![0], force: false, beFailed: false) rebalancePosition(signer: protocolAccount, pid: pid, force: false, beFailed: false) yieldVaultBalance = getYieldVaultBalance(address: user.address, yieldVaultID: yieldVaultIDs![0]) diff --git a/cadence/tests/rebalance_scenario2_test.cdc b/cadence/tests/rebalance_scenario2_test.cdc index 51b48b47..06449882 100644 --- a/cadence/tests/rebalance_scenario2_test.cdc +++ b/cadence/tests/rebalance_scenario2_test.cdc @@ -188,7 +188,6 @@ fun test_RebalanceYieldVaultScenario2() { log("[TEST] Initial yield vault balance: \(yieldVaultBalance ?? 0.0)") - rebalanceYieldVault(signer: flowYieldVaultsAccount, id: yieldVaultIDs![0], force: true, beFailed: false) rebalancePosition(signer: protocolAccount, pid: pid, force: true, beFailed: false) for index, yieldTokenPrice in yieldPriceIncreases { diff --git a/cadence/tests/rebalance_scenario3a_test.cdc b/cadence/tests/rebalance_scenario3a_test.cdc index f111fb58..4a773049 100644 --- a/cadence/tests/rebalance_scenario3a_test.cdc +++ b/cadence/tests/rebalance_scenario3a_test.cdc @@ -153,7 +153,6 @@ fun test_RebalanceYieldVaultScenario3A() { message: "Expected MOET debt to be \(expectedDebtValues[0]) but got \(debtBefore)" ) - rebalanceYieldVault(signer: flowYieldVaultsAccount, id: yieldVaultIDs![0], force: true, beFailed: false) rebalancePosition(signer: protocolAccount, pid: pid, force: true, beFailed: false) // Debug: Log position details diff --git a/cadence/tests/rebalance_scenario3b_test.cdc b/cadence/tests/rebalance_scenario3b_test.cdc index 8a521dbc..8fe741fe 100644 --- a/cadence/tests/rebalance_scenario3b_test.cdc +++ b/cadence/tests/rebalance_scenario3b_test.cdc @@ -153,7 +153,6 @@ fun test_RebalanceYieldVaultScenario3B() { message: "Expected MOET debt to be \(expectedDebtValues[0]) but got \(debtBefore)" ) - rebalanceYieldVault(signer: flowYieldVaultsAccount, id: yieldVaultIDs![0], force: true, beFailed: false) rebalancePosition(signer: protocolAccount, pid: pid, force: true, beFailed: false) let yieldTokensAfterFlowPriceIncrease = getAutoBalancerBalance(id: yieldVaultIDs![0])! diff --git a/cadence/tests/rebalance_scenario3c_test.cdc b/cadence/tests/rebalance_scenario3c_test.cdc index 06514569..84e4a5a7 100644 --- a/cadence/tests/rebalance_scenario3c_test.cdc +++ b/cadence/tests/rebalance_scenario3c_test.cdc @@ -153,7 +153,6 @@ fun test_RebalanceYieldVaultScenario3C() { message: "Expected MOET debt to be \(expectedDebtValues[0]) but got \(debtBefore)" ) - rebalanceYieldVault(signer: flowYieldVaultsAccount, id: yieldVaultIDs![0], force: true, beFailed: false) rebalancePosition(signer: protocolAccount, pid: pid, force: true, beFailed: false) let yieldTokensAfterFlowPriceIncrease = getAutoBalancerBalance(id: yieldVaultIDs![0])! diff --git a/cadence/tests/rebalance_scenario3d_test.cdc b/cadence/tests/rebalance_scenario3d_test.cdc index 38fefdba..2def8b74 100644 --- a/cadence/tests/rebalance_scenario3d_test.cdc +++ b/cadence/tests/rebalance_scenario3d_test.cdc @@ -153,7 +153,6 @@ fun test_RebalanceYieldVaultScenario3D() { message: "Expected MOET debt to be \(expectedDebtValues[0]) but got \(debtBefore)" ) - rebalanceYieldVault(signer: flowYieldVaultsAccount, id: yieldVaultIDs![0], force: true, beFailed: false) rebalancePosition(signer: protocolAccount, pid: pid, force: true, beFailed: false) let yieldTokensAfterFlowPriceDecrease = getAutoBalancerBalance(id: yieldVaultIDs![0])! diff --git a/cadence/tests/rebalance_scenario4_test.cdc b/cadence/tests/rebalance_scenario4_test.cdc index b1a2000c..397b6d7e 100644 --- a/cadence/tests/rebalance_scenario4_test.cdc +++ b/cadence/tests/rebalance_scenario4_test.cdc @@ -133,7 +133,6 @@ fun test_RebalanceLowCollateralHighYieldPrices() { log(" FLOW collateral: \(collateralBefore) FLOW (value: \(collateralBefore * flowPriceDecrease) MOET @ $\(flowPriceDecrease)/FLOW)") log(" MOET debt: \(debtBefore) MOET") - rebalanceYieldVault(signer: flowYieldVaultsAccount, id: yieldVaultIDs![0], force: true, beFailed: false) rebalancePosition(signer: protocolAccount, pid: pid, force: true, beFailed: false) let ytAfterFlowDrop = getAutoBalancerBalance(id: yieldVaultIDs![0])! @@ -258,8 +257,7 @@ fun test_RebalanceHighCollateralLowYieldPrices() { message: "Expected health to remain above \(SOLVENT_HEALTH_FLOOR) after 20% FLOW price drop, got \(healthBeforeRebalance)") // Rebalance to restore health to the strategy target. - log("[Scenario5] Rebalancing position and yield vault...") - rebalanceYieldVault(signer: flowYieldVaultsAccount, id: yieldVaultIDs![0], force: true, beFailed: false) + log("[Scenario5] Rebalancing position...") rebalancePosition(signer: protocolAccount, pid: pid, force: true, beFailed: false) let ytAfterFlowDrop = getAutoBalancerBalance(id: yieldVaultIDs![0])! diff --git a/cadence/tests/rebalance_yield_test.cdc b/cadence/tests/rebalance_yield_test.cdc index aaac6fd5..3139f929 100644 --- a/cadence/tests/rebalance_yield_test.cdc +++ b/cadence/tests/rebalance_yield_test.cdc @@ -113,7 +113,6 @@ fun test_RebalanceYieldVaultScenario2() { log("[TEST] Initial yield vault balance: \(yieldVaultBalance ?? 0.0)") - rebalanceYieldVault(signer: flowYieldVaultsAccount, id: yieldVaultIDs![0], force: true, beFailed: false) rebalancePosition(signer: protocolAccount, pid: pid, force: true, beFailed: false) for index, yieldTokenPrice in yieldPriceIncreases { diff --git a/cadence/tests/tracer_strategy_test.cdc b/cadence/tests/tracer_strategy_test.cdc index 27d5a59d..3c497094 100644 --- a/cadence/tests/tracer_strategy_test.cdc +++ b/cadence/tests/tracer_strategy_test.cdc @@ -398,11 +398,8 @@ fun test_RebalanceYieldVaultSucceedsAfterCollateralPriceIncrease() { log("Yield token balance before rebalance: \(yieldTokensBefore)") - // Rebalance the YieldVault to adjust the Yield tokens based on the new collateral price - // Force both yield vault and position to rebalance - rebalanceYieldVault(signer: flowYieldVaultsAccount, id: yieldVaultIDs![0], force: true, beFailed: false) - - // Position ID is hardcoded to 1 here since this is the first yield vault created, + // Position health increased because FLOW collateral is worth more; drawDown brings it back to target. + // Position ID is hardcoded to 1 here since this is the first yield vault created, // if there is a better way to get the position ID, please let me know rebalancePosition(signer: protocolAccount, pid: positionID, force: true, beFailed: false) From da49a3fc5dd6a391bc39e4f7a5964c1b46f12873 Mon Sep 17 00:00:00 2001 From: Alex <12097569+nialexsan@users.noreply.github.com> Date: Mon, 9 Mar 2026 22:09:36 -0400 Subject: [PATCH 15/27] fix setup emulator script --- local/setup_emulator.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/local/setup_emulator.sh b/local/setup_emulator.sh index 730950c2..d7d8f990 100755 --- a/local/setup_emulator.sh +++ b/local/setup_emulator.sh @@ -41,7 +41,7 @@ flow transactions send ./cadence/transactions/mocks/swapper/set_liquidity_connec flow transactions send ./cadence/transactions/flow-yield-vaults/admin/add_strategy_composer.cdc \ 'A.045a1763c93006ca.MockStrategies.TracerStrategy' \ 'A.045a1763c93006ca.MockStrategies.TracerStrategyComposer' \ - /storage/FlowYieldVaultsStrategyComposerIssuer_0x045a1763c93006ca \ + /storage/MockStrategiesComposerIssuer_0x045a1763c93006ca \ --signer emulator-flow-yield-vaults # flow transactions send ../cadence/transactions/flow-yield-vaults/admin/upsert_musdf_config.cdc \ From 817cbea98135aa34ffa4b1050a94160ae4969fee Mon Sep 17 00:00:00 2001 From: Alex <12097569+nialexsan@users.noreply.github.com> Date: Mon, 9 Mar 2026 22:47:43 -0400 Subject: [PATCH 16/27] tweak test --- ...wYieldVaultsStrategiesV2_syWFLOWv_test.cdc | 24 ++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) diff --git a/cadence/tests/FlowYieldVaultsStrategiesV2_syWFLOWv_test.cdc b/cadence/tests/FlowYieldVaultsStrategiesV2_syWFLOWv_test.cdc index d901cec2..38ec38ca 100644 --- a/cadence/tests/FlowYieldVaultsStrategiesV2_syWFLOWv_test.cdc +++ b/cadence/tests/FlowYieldVaultsStrategiesV2_syWFLOWv_test.cdc @@ -1,4 +1,4 @@ -#test_fork(network: "mainnet", height: nil) +#test_fork(network: "mainnet", height: 144698401) import Test @@ -291,6 +291,28 @@ access(all) fun setup() { ) 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...") From 3a8962aa2302a60f97c7558bfc566a3b52a5c3be Mon Sep 17 00:00:00 2001 From: Alex <12097569+nialexsan@users.noreply.github.com> Date: Tue, 10 Mar 2026 13:40:28 -0400 Subject: [PATCH 17/27] update FlowALPv0 ref --- lib/FlowALP | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/FlowALP b/lib/FlowALP index 02c6a92c..7ceac067 160000 --- a/lib/FlowALP +++ b/lib/FlowALP @@ -1 +1 @@ -Subproject commit 02c6a92cc4f6d1210e3db19c617db9916c3ad388 +Subproject commit 7ceac06706e288b254a8ddf448540c7d3538a5d5 From f41e7d42947da3b8970df4f9f0c120abc9249594 Mon Sep 17 00:00:00 2001 From: Alex <12097569+nialexsan@users.noreply.github.com> Date: Tue, 10 Mar 2026 16:40:22 -0400 Subject: [PATCH 18/27] additional logic --- .../contracts/FlowYieldVaultsStrategiesV2.cdc | 537 +++++++++++++++++- ...wYieldVaultsStrategiesV2_syWFLOWv_test.cdc | 2 +- 2 files changed, 520 insertions(+), 19 deletions(-) diff --git a/cadence/contracts/FlowYieldVaultsStrategiesV2.cdc b/cadence/contracts/FlowYieldVaultsStrategiesV2.cdc index 65993226..777714c4 100644 --- a/cadence/contracts/FlowYieldVaultsStrategiesV2.cdc +++ b/cadence/contracts/FlowYieldVaultsStrategiesV2.cdc @@ -45,9 +45,16 @@ access(all) contract FlowYieldVaultsStrategiesV2 { /// Partitioned config map. Each key is a partition name; each value is a typed nested map keyed by /// strategy UniqueIdentifier ID (UInt64). Current partitions: - /// "yieldToMoetSwappers" → {UInt64: {DeFiActions.Swapper}} - /// "debtToCollateralSwappers" → {UInt64: {DeFiActions.Swapper}} - /// "closedPositions" → {UInt64: Bool} + /// "yieldToMoetSwappers" → {UInt64: {DeFiActions.Swapper}} + /// "debtToCollateralSwappers" → {UInt64: {DeFiActions.Swapper}} + /// "collateralToDebtSwappers" → {UInt64: {DeFiActions.Swapper}} + /// "closedPositions" → {UInt64: Bool} + /// "syWFLOWvDebtTokenTypes" → {UInt64: Type} + /// "moreERC4626Configs" → {Type: {Type: {Type: MoreERC4626CollateralConfig}}} + /// "moetPreswapConfigs" → {Type: {Type: MoetPreswapConfig}} + /// "originalCollateralTypes" → {UInt64: Type} + /// "collateralPreSwappers" → {UInt64: {DeFiActions.Swapper}} + /// "moetToCollateralSwappers" → {UInt64: {DeFiActions.Swapper}} access(contract) let config: {String: AnyStruct} /// Canonical StoragePath where the StrategyComposerIssuer should be stored @@ -145,6 +152,35 @@ access(all) contract FlowYieldVaultsStrategiesV2 { } } + /// Configuration for pre-swapping a stablecoin collateral to MOET before depositing into + /// FlowALP. Required when the collateral type is not directly supported by FlowALP (e.g. + /// PYUSD0 must be swapped to MOET since FlowALP only supports MOET as its stablecoin). + /// + /// The path is collateral → MOET (e.g. [PYUSD0_addr, MOET_addr] for a 1-hop swap, or + /// [PYUSD0_addr, WFLOW_addr, MOET_addr] for a 2-hop swap). The reverse (MOET→collateral) + /// is derived automatically by reversing both arrays. + access(all) struct MoetPreswapConfig { + /// Full UniV3 swap path: collateral EVM address → ... → MOET EVM address. + /// First element is the collateral, last element must be MOET. + access(all) let collateralToMoetAddressPath: [EVM.EVMAddress] + /// UniV3 fee tiers for each hop (length must equal addressPath.length - 1). + access(all) let collateralToMoetFeePath: [UInt32] + + init( + collateralToMoetAddressPath: [EVM.EVMAddress], + collateralToMoetFeePath: [UInt32] + ) { + pre { + collateralToMoetAddressPath.length > 1: + "MoetPreswapConfig: path must have at least 2 elements (collateral + MOET)" + collateralToMoetFeePath.length == collateralToMoetAddressPath.length - 1: + "MoetPreswapConfig: fee path length must equal address path length - 1" + } + self.collateralToMoetAddressPath = collateralToMoetAddressPath + self.collateralToMoetFeePath = collateralToMoetFeePath + } + } + /// Collateral configuration for strategies that borrow the vault's underlying asset directly, /// using a standard ERC4626 deposit for the forward path (underlying → yield token) and a /// UniV3 AMM swap for the reverse path (yield token → underlying). This applies to "More" @@ -204,20 +240,59 @@ access(all) contract FlowYieldVaultsStrategiesV2 { // access(all) view fun isSupportedCollateralType(_ type: Type): Bool access(all) view fun getSupportedCollateralTypes(): {Type: Bool} { + // If this strategy was initialized with a stablecoin pre-swap (e.g. PYUSD0→MOET), + // expose the original (external) collateral type to callers, not the internal MOET type. + if let id = self.uniqueID { + if let originalType = FlowYieldVaultsStrategiesV2._getOriginalCollateralType(id.id) { + return { originalType: true } + } + } return { self.sink.getSinkType(): true } } /// Returns the amount available for withdrawal via the inner Source access(all) fun availableBalance(ofToken: Type): UFix64 { if FlowYieldVaultsStrategiesV2._isPositionClosed(self.uniqueID) { return 0.0 } - return ofToken == self.source.getSourceType() ? self.source.minimumAvailable() : 0.0 + // If stablecoin pre-swap is in effect, match against the original (external) collateral type. + // MOET and PYUSD0 are both stablecoins with approximately equal value (1:1), so the MOET + // balance is a reasonable approximation of the PYUSD0-denominated collateral balance. + var effectiveSourceType = self.source.getSourceType() + if let id = self.uniqueID { + if let originalType = FlowYieldVaultsStrategiesV2._getOriginalCollateralType(id.id) { + effectiveSourceType = originalType + } + } + return ofToken == effectiveSourceType ? self.source.minimumAvailable() : 0.0 } /// Deposits up to the inner Sink's capacity from the provided authorized Vault reference. - /// Only the single configured collateral type is accepted — one collateral type per position. + /// Accepts both the internal collateral type (MOET) and, when a pre-swap is configured, + /// the original external collateral type (e.g. PYUSD0) — which is swapped to MOET first. access(all) fun deposit(from: auth(FungibleToken.Withdraw) &{FungibleToken.Vault}) { pre { - from.getType() == self.sink.getSinkType(): + from.getType() == self.sink.getSinkType() + || (self.uniqueID != nil && FlowYieldVaultsStrategiesV2._getOriginalCollateralType(self.uniqueID!.id) == from.getType()): "FUSDEVStrategy position only accepts \(self.sink.getSinkType().identifier) as collateral, got \(from.getType().identifier)" } + // If depositing the original stablecoin collateral (e.g. PYUSD0), pre-swap to MOET + if let id = self.uniqueID { + if from.getType() != self.sink.getSinkType() { + if let preSwapper = FlowYieldVaultsStrategiesV2._getCollateralPreSwapper(id.id) { + let incoming <- from.withdraw(amount: from.balance) + if incoming.balance > 0.0 { + let quote = preSwapper.quoteOut(forProvided: incoming.balance, reverse: false) + if quote.outAmount > 0.0 { + let moetVault <- preSwapper.swap(quote: quote, inVault: <-incoming) + self.sink.depositCapacity(from: &moetVault as auth(FungibleToken.Withdraw) &{FungibleToken.Vault}) + Burner.burn(<-moetVault) + } else { + Burner.burn(<-incoming) + } + } else { + Burner.burn(<-incoming) + } + return + } + } + } self.sink.depositCapacity(from: from) } /// Withdraws up to the max amount, returning the withdrawn Vault. If the requested token type is unsupported, @@ -281,8 +356,24 @@ access(all) contract FlowYieldVaultsStrategiesV2 { FlowYieldVaultsStrategiesV2._markPositionClosed(self.uniqueID) return <- DeFiActionsUtils.getEmptyVault(collateralType) } - let collateralVault <- resultVaults.removeFirst() + var collateralVault <- resultVaults.removeFirst() destroy resultVaults + // Convert internal collateral (MOET) → external collateral (e.g. PYUSD0) if needed + if let id = self.uniqueID { + if let moetToOrigSwapper = FlowYieldVaultsStrategiesV2._getMoetToCollateralSwapper(id.id) { + if collateralVault.balance > 0.0 { + let quote = moetToOrigSwapper.quoteOut(forProvided: collateralVault.balance, reverse: false) + if quote.outAmount > 0.0 { + let extVault <- moetToOrigSwapper.swap(quote: quote, inVault: <-collateralVault) + FlowYieldVaultsStrategiesV2._markPositionClosed(self.uniqueID) + return <- extVault + } + } + Burner.burn(<-collateralVault) + FlowYieldVaultsStrategiesV2._markPositionClosed(self.uniqueID) + return <- DeFiActionsUtils.getEmptyVault(collateralType) + } + } FlowYieldVaultsStrategiesV2._markPositionClosed(self.uniqueID) return <- collateralVault } @@ -333,6 +424,21 @@ access(all) contract FlowYieldVaultsStrategiesV2 { Burner.burn(<-extraCollateral) } } + } else { + if let id = self.uniqueID { + if FlowYieldVaultsStrategiesV2._getOriginalCollateralType(id.id) != nil { + // Stablecoin pre-swap case: position collateral IS MOET (same as debt token). + // Pull MOET directly from the collateral source to pre-reduce debt — no swap needed. + let shortfall = totalDebtAmount - expectedMOET + let buffered = shortfall + shortfall / 100.0 + let extraMOET <- self.source.withdrawAvailable(maxAmount: buffered) + if extraMOET.balance > 0.0 { + self.position.deposit(from: <-extraMOET) + } else { + Burner.burn(<-extraMOET) + } + } + } } } @@ -386,6 +492,13 @@ access(all) contract FlowYieldVaultsStrategiesV2 { access(contract) fun burnCallback() { FlowYieldVaultsAutoBalancers._cleanupAutoBalancer(id: self.id()!) FlowYieldVaultsStrategiesV2._cleanupPositionClosed(self.uniqueID) + // Clean up stablecoin pre-swap config entries (no-op if not set) + if let id = self.uniqueID { + FlowYieldVaultsStrategiesV2._removeOriginalCollateralType(id.id) + FlowYieldVaultsStrategiesV2._removeCollateralPreSwapper(id.id) + FlowYieldVaultsStrategiesV2._removeMoetToCollateralSwapper(id.id) + FlowYieldVaultsStrategiesV2._removeDebtToCollateralSwapper(id.id) + } } access(all) fun getComponentInfo(): DeFiActions.ComponentInfo { return DeFiActions.ComponentInfo( @@ -447,18 +560,36 @@ access(all) contract FlowYieldVaultsStrategiesV2 { // access(all) view fun isSupportedCollateralType(_ type: Type): Bool access(all) view fun getSupportedCollateralTypes(): {Type: Bool} { + // If this strategy was initialized with a stablecoin pre-swap (e.g. PYUSD0→MOET), + // expose the original (external) collateral type to callers, not the internal MOET type. + if let id = self.uniqueID { + if let originalType = FlowYieldVaultsStrategiesV2._getOriginalCollateralType(id.id) { + return { originalType: true } + } + } return { self.sink.getSinkType(): true } } /// Returns the amount available for withdrawal via the inner Source access(all) fun availableBalance(ofToken: Type): UFix64 { if self.positionClosed { return 0.0 } - return ofToken == self.source.getSourceType() ? self.source.minimumAvailable() : 0.0 + // If stablecoin pre-swap is in effect, match against the original (external) collateral type. + // MOET and the stablecoin collateral (e.g. PYUSD0) have approximately equal value (1:1). + var effectiveSourceType = self.source.getSourceType() + if let id = self.uniqueID { + if let originalType = FlowYieldVaultsStrategiesV2._getOriginalCollateralType(id.id) { + effectiveSourceType = originalType + } + } + return ofToken == effectiveSourceType ? self.source.minimumAvailable() : 0.0 } /// Deposits up to the inner Sink's capacity from the provided authorized Vault reference. + /// Accepts both the internal collateral type and, when a pre-swap is configured, the original + /// external collateral type (e.g. PYUSD0) — which is swapped to MOET first. /// FLOW cannot be used as collateral — it is the vault's underlying asset (the debt token). access(all) fun deposit(from: auth(FungibleToken.Withdraw) &{FungibleToken.Vault}) { pre { - from.getType() == self.sink.getSinkType(): + from.getType() == self.sink.getSinkType() + || (self.uniqueID != nil && FlowYieldVaultsStrategiesV2._getOriginalCollateralType(self.uniqueID!.id) == from.getType()): "syWFLOWvStrategy position only accepts \(self.sink.getSinkType().identifier) as collateral, got \(from.getType().identifier)" } // Reject the debt token (FLOW) as collateral — looked up from contract-level config @@ -469,6 +600,27 @@ access(all) contract FlowYieldVaultsStrategiesV2 { message: "syWFLOWvStrategy: FLOW cannot be used as collateral — it is the vault's underlying asset" ) } + // If depositing the original stablecoin collateral (e.g. PYUSD0), pre-swap to MOET + if let id = self.uniqueID { + if from.getType() != self.sink.getSinkType() { + if let preSwapper = FlowYieldVaultsStrategiesV2._getCollateralPreSwapper(id.id) { + let incoming <- from.withdraw(amount: from.balance) + if incoming.balance > 0.0 { + let quote = preSwapper.quoteOut(forProvided: incoming.balance, reverse: false) + if quote.outAmount > 0.0 { + let moetVault <- preSwapper.swap(quote: quote, inVault: <-incoming) + self.sink.depositCapacity(from: &moetVault as auth(FungibleToken.Withdraw) &{FungibleToken.Vault}) + Burner.burn(<-moetVault) + } else { + Burner.burn(<-incoming) + } + } else { + Burner.burn(<-incoming) + } + return + } + } + } self.sink.depositCapacity(from: from) } /// Withdraws up to the max amount, returning the withdrawn Vault. If the requested token type is unsupported, @@ -480,7 +632,9 @@ access(all) contract FlowYieldVaultsStrategiesV2 { 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. + /// (via the stored yield→FLOW swapper) and closing with them. When a stablecoin pre-swap is + /// configured (e.g. PYUSD0→MOET), the returned MOET collateral is swapped back to the + /// original stablecoin before returning. access(FungibleToken.Withdraw) fun closePosition(collateralType: Type): @{FungibleToken.Vault} { pre { self.isSupportedCollateralType(collateralType): @@ -490,6 +644,14 @@ access(all) contract FlowYieldVaultsStrategiesV2 { result.getType() == collateralType: "Withdraw Vault (\(result.getType().identifier)) is not of a requested collateral type (\(collateralType.identifier))" } + // Determine the internal collateral type (may be MOET if a pre-swap was applied) + var internalCollateralType = collateralType + if let id = self.uniqueID { + if FlowYieldVaultsStrategiesV2._getOriginalCollateralType(id.id) != nil { + internalCollateralType = self.sink.getSinkType() // MOET + } + } + // Step 1: Get debt amounts let debtsByType = self.position.getTotalDebt() @@ -515,8 +677,29 @@ access(all) contract FlowYieldVaultsStrategiesV2 { self.positionClosed = true return <- DeFiActionsUtils.getEmptyVault(collateralType) } - let collateralVault <- resultVaults.removeFirst() + var collateralVault <- resultVaults.removeFirst() destroy resultVaults + // Convert internal collateral (MOET) → external collateral (e.g. PYUSD0) if needed + if internalCollateralType != collateralType { + if let id = self.uniqueID { + if let moetToOrigSwapper = FlowYieldVaultsStrategiesV2._getMoetToCollateralSwapper(id.id) { + if collateralVault.balance > 0.0 { + let quote = moetToOrigSwapper.quoteOut(forProvided: collateralVault.balance, reverse: false) + if quote.outAmount > 0.0 { + let extVault <- moetToOrigSwapper.swap(quote: quote, inVault: <-collateralVault) + self.positionClosed = true + return <- extVault + } + } + Burner.burn(<-collateralVault) + self.positionClosed = true + return <- DeFiActionsUtils.getEmptyVault(collateralType) + } + } + Burner.burn(<-collateralVault) + self.positionClosed = true + return <- DeFiActionsUtils.getEmptyVault(collateralType) + } self.positionClosed = true return <- collateralVault } @@ -542,15 +725,15 @@ access(all) contract FlowYieldVaultsStrategiesV2 { var collateralVault <- resultVaults.removeFirst() assert( - collateralVault.getType() == collateralType, - message: "First vault returned from closePosition must be collateral (\(collateralType.identifier)), got \(collateralVault.getType().identifier)" + collateralVault.getType() == internalCollateralType, + message: "First vault returned from closePosition must be internal collateral (\(internalCollateralType.identifier)), got \(collateralVault.getType().identifier)" ) // Handle any overpayment dust (FLOW) returned as the second vault. while resultVaults.length > 0 { let dustVault <- resultVaults.removeFirst() if dustVault.balance > 0.0 { - if dustVault.getType() == collateralType { + if dustVault.getType() == internalCollateralType { collateralVault.deposit(from: <-dustVault) } else { // Quote first — if dust is too small to route, destroy it @@ -568,6 +751,26 @@ access(all) contract FlowYieldVaultsStrategiesV2 { } destroy resultVaults + + // Convert internal collateral (MOET) → external collateral (e.g. PYUSD0) if needed + if internalCollateralType != collateralType { + if let id = self.uniqueID { + if let moetToOrigSwapper = FlowYieldVaultsStrategiesV2._getMoetToCollateralSwapper(id.id) { + if collateralVault.balance > 0.0 { + let quote = moetToOrigSwapper.quoteOut(forProvided: collateralVault.balance, reverse: false) + if quote.outAmount > 0.0 { + let extVault <- moetToOrigSwapper.swap(quote: quote, inVault: <-collateralVault) + self.positionClosed = true + return <- extVault + } + } + } + } + Burner.burn(<-collateralVault) + self.positionClosed = true + return <- DeFiActionsUtils.getEmptyVault(collateralType) + } + self.positionClosed = true return <- collateralVault } @@ -575,6 +778,12 @@ access(all) contract FlowYieldVaultsStrategiesV2 { access(contract) fun burnCallback() { FlowYieldVaultsAutoBalancers._cleanupAutoBalancer(id: self.id()!) FlowYieldVaultsStrategiesV2._removeSyWFLOWvDebtTokenType(self.uniqueID?.id) + // Clean up stablecoin pre-swap config entries (no-op if not set) + if let id = self.uniqueID { + FlowYieldVaultsStrategiesV2._removeOriginalCollateralType(id.id) + FlowYieldVaultsStrategiesV2._removeCollateralPreSwapper(id.id) + FlowYieldVaultsStrategiesV2._removeMoetToCollateralSwapper(id.id) + } } access(all) fun getComponentInfo(): DeFiActions.ComponentInfo { return DeFiActions.ComponentInfo( @@ -850,6 +1059,80 @@ access(all) contract FlowYieldVaultsStrategiesV2 { uniqueID: uniqueID ) + // Store yield→MOET swapper for later access during closePosition + FlowYieldVaultsStrategiesV2._setYieldToMoetSwapper(uniqueID.id, yieldToDebtSwapper) + + // --- Stablecoin pre-swap path (e.g. PYUSD0 → MOET) --- + // When configured, swap collateral to MOET before depositing into FlowALP, since + // FlowALP only supports MOET as its stablecoin collateral (not PYUSD0 etc.). + if let preswapCfg = FlowYieldVaultsStrategiesV2._getMoetPreswapConfig( + composer: Type<@MorphoERC4626StrategyComposer>(), + collateral: collateralType + ) { + let preSwapper = self._createUniV3Swapper( + tokenPath: preswapCfg.collateralToMoetAddressPath, + feePath: preswapCfg.collateralToMoetFeePath, + inVault: collateralType, + outVault: tokens.moetTokenType, + uniqueID: uniqueID + ) + let preSwapQuote = preSwapper.quoteOut(forProvided: withFunds.balance, reverse: false) + let moetFunds <- preSwapper.swap(quote: preSwapQuote, inVault: <-withFunds) + + // Open FlowALPv0 position with MOET as collateral + let position <- FlowYieldVaultsStrategiesV2._openCreditPosition( + funds: <-moetFunds, + issuanceSink: abaSwapSink, + repaymentSource: abaSwapSource + ) + + // AutoBalancer rebalancing via MOET collateral: + // Overflow: sell FUSDEV → MOET → add to position collateral + // Deficit: pull MOET from position → buy FUSDEV + let positionSink = position.createSinkWithOptions(type: tokens.moetTokenType, pushToDrawDownSink: true) + let positionSwapSink = SwapConnectors.SwapSink( + swapper: yieldToDebtSwapper, // FUSDEV → MOET + sink: positionSink, + uniqueID: uniqueID + ) + let positionSource = position.createSourceWithOptions(type: tokens.moetTokenType, pullFromTopUpSource: false) + let positionSwapSource = SwapConnectors.SwapSource( + swapper: debtToYieldSwapper, // MOET → FUSDEV + source: positionSource, + uniqueID: uniqueID + ) + balancerIO.autoBalancer.setSink(positionSwapSink, updateSinkID: true) + balancerIO.autoBalancer.setSource(positionSwapSource, updateSourceID: true) + + // Store original collateral type (PYUSD0) and pre-swapper for deposit/close + FlowYieldVaultsStrategiesV2._setOriginalCollateralType(uniqueID.id, collateralType) + FlowYieldVaultsStrategiesV2._setCollateralPreSwapper(uniqueID.id, preSwapper) + + // MOET → collateral (e.g. PYUSD0): use the preswap path in reverse. + // Per design: "use the same swapper in reverse during close position". + let moetToOrigCollateral = self._createUniV3Swapper( + tokenPath: preswapCfg.collateralToMoetAddressPath.reverse(), + feePath: preswapCfg.collateralToMoetFeePath.reverse(), + inVault: tokens.moetTokenType, + outVault: collateralType, + uniqueID: uniqueID + ) + // Store under both partitions: moetToCollateralSwappers (for the no-debt close + // path) and debtToCollateralSwappers (for the regular close path MOET dust). + FlowYieldVaultsStrategiesV2._setMoetToCollateralSwapper(uniqueID.id, moetToOrigCollateral) + FlowYieldVaultsStrategiesV2._setDebtToCollateralSwapper(uniqueID.id, moetToOrigCollateral) + // Note: _setCollateralToDebtSwapper is NOT set for stablecoin (MOET) collateral. + // The MOET-direct pre-supplement path in closePosition handles this case. + + return <-create FUSDEVStrategy( + id: uniqueID, + collateralType: tokens.moetTokenType, + position: <-position + ) + } + + // --- Standard path (WBTC, WETH — directly supported by FlowALP) --- + // Open FlowALPv0 position let position <- FlowYieldVaultsStrategiesV2._openCreditPosition( funds: <-withFunds, @@ -899,9 +1182,6 @@ access(all) contract FlowYieldVaultsStrategiesV2 { balancerIO.autoBalancer.setSink(positionSwapSink, updateSinkID: true) balancerIO.autoBalancer.setSource(positionSwapSource, updateSourceID: true) - // Store yield→MOET swapper for later access during closePosition - FlowYieldVaultsStrategiesV2._setYieldToMoetSwapper(uniqueID.id, yieldToDebtSwapper) - // Store collateral→MOET swapper for pre-supplement in closePosition. // Used to cover the ~0.02% round-trip fee shortfall when yield hasn't accrued. let collateralToDebtSwapper = self._createCollateralToDebtSwapper( @@ -1244,7 +1524,6 @@ access(all) contract FlowYieldVaultsStrategiesV2 { ) // For syWFLOWvStrategy the debt token IS the underlying asset (FLOW), not MOET. - // Use tokens.underlying4626AssetType directly wherever the debt token type is needed. let flowDebtTokenType = tokens.underlying4626AssetType // FLOW → syWFLOWv: standard ERC4626 deposit (More vault, not Morpho — no AMM needed) @@ -1281,6 +1560,96 @@ access(all) contract FlowYieldVaultsStrategiesV2 { uniqueID: uniqueID ) + // --- Stablecoin pre-swap path (e.g. PYUSD0 → MOET) --- + if let preswapCfg = FlowYieldVaultsStrategiesV2._getMoetPreswapConfig( + composer: Type<@MoreERC4626StrategyComposer>(), + collateral: collateralType + ) { + let preSwapper = UniswapV3SwapConnectors.Swapper( + factoryAddress: FlowYieldVaultsStrategiesV2.univ3FactoryEVMAddress, + routerAddress: FlowYieldVaultsStrategiesV2.univ3RouterEVMAddress, + quoterAddress: FlowYieldVaultsStrategiesV2.univ3QuoterEVMAddress, + tokenPath: preswapCfg.collateralToMoetAddressPath, + feePath: preswapCfg.collateralToMoetFeePath, + inVault: collateralType, + outVault: tokens.moetTokenType, + coaCapability: FlowYieldVaultsStrategiesV2._getCOACapability(), + uniqueID: uniqueID + ) + let preSwapQuote = preSwapper.quoteOut(forProvided: withFunds.balance, reverse: false) + let moetFunds <- preSwapper.swap(quote: preSwapQuote, inVault: <-withFunds) + + // Open FlowALP position with MOET as collateral + let positionPreswap <- FlowYieldVaultsStrategiesV2._openCreditPosition( + funds: <-moetFunds, + issuanceSink: abaSwapSinkFlow, + repaymentSource: abaSwapSourceFlow + ) + + // AutoBalancer debt management: same as WBTC/WETH (manages FLOW borrow/repay) + let positionDebtSinkPre = positionPreswap.createSinkWithOptions( + type: flowDebtTokenType, + pushToDrawDownSink: false + ) + let positionDebtSwapSinkPre = SwapConnectors.SwapSink( + swapper: syWFLOWvToFlow, + sink: positionDebtSinkPre, + uniqueID: uniqueID + ) + let positionDebtSourcePre = positionPreswap.createSourceWithOptions( + type: flowDebtTokenType, + pullFromTopUpSource: false + ) + let positionDebtSwapSourcePre = SwapConnectors.SwapSource( + swapper: flowToSyWFLOWv, + source: positionDebtSourcePre, + uniqueID: uniqueID + ) + balancerIO.autoBalancer.setSink(positionDebtSwapSinkPre, updateSinkID: true) + balancerIO.autoBalancer.setSource(positionDebtSwapSourcePre, updateSourceID: true) + + // FLOW→MOET: converts FLOW dust back to MOET (internal collateral) in closePosition + let flowToMoet = UniswapV3SwapConnectors.Swapper( + factoryAddress: FlowYieldVaultsStrategiesV2.univ3FactoryEVMAddress, + routerAddress: FlowYieldVaultsStrategiesV2.univ3RouterEVMAddress, + quoterAddress: FlowYieldVaultsStrategiesV2.univ3QuoterEVMAddress, + tokenPath: collateralConfig.debtToCollateralUniV3AddressPath, + feePath: collateralConfig.debtToCollateralUniV3FeePath, + inVault: tokens.underlying4626AssetType, // FLOW + outVault: tokens.moetTokenType, // MOET + coaCapability: FlowYieldVaultsStrategiesV2._getCOACapability(), + uniqueID: uniqueID + ) + + // MOET→collateral (e.g. PYUSD0): final conversion in closePosition + let moetToCollateral = UniswapV3SwapConnectors.Swapper( + factoryAddress: FlowYieldVaultsStrategiesV2.univ3FactoryEVMAddress, + routerAddress: FlowYieldVaultsStrategiesV2.univ3RouterEVMAddress, + quoterAddress: FlowYieldVaultsStrategiesV2.univ3QuoterEVMAddress, + tokenPath: preswapCfg.collateralToMoetAddressPath.reverse(), + feePath: preswapCfg.collateralToMoetFeePath.reverse(), + inVault: tokens.moetTokenType, + outVault: collateralType, + coaCapability: FlowYieldVaultsStrategiesV2._getCOACapability(), + uniqueID: uniqueID + ) + + FlowYieldVaultsStrategiesV2._setSyWFLOWvDebtTokenType(uniqueID.id, flowDebtTokenType) + FlowYieldVaultsStrategiesV2._setOriginalCollateralType(uniqueID.id, collateralType) + FlowYieldVaultsStrategiesV2._setCollateralPreSwapper(uniqueID.id, preSwapper) + FlowYieldVaultsStrategiesV2._setMoetToCollateralSwapper(uniqueID.id, moetToCollateral) + + return <-create syWFLOWvStrategy( + id: uniqueID, + collateralType: tokens.moetTokenType, + position: <-positionPreswap, + yieldToDebtSwapper: syWFLOWvToFlow, + debtToCollateralSwapper: flowToMoet + ) + } + + // --- Standard path (WBTC, WETH — directly supported by FlowALP) --- + // Open FlowALP position with collateral; drawDownSink accepts FLOW let positionFlow <- FlowYieldVaultsStrategiesV2._openCreditPosition( funds: <-withFunds, @@ -1554,6 +1923,39 @@ access(all) contract FlowYieldVaultsStrategiesV2 { self.upsertMoreERC4626Config(config: { strategyType: { collateralVaultType: cfg } }) } + /// Configures a stablecoin collateral type to use MOET pre-swap before depositing into + /// FlowALP. Required for stablecoins like PYUSD0 that FlowALP does not support directly + /// as collateral (it only accepts MOET as its stablecoin). + /// + /// collateralToMoetAddressPath: full UniV3 path from collateral EVM address → MOET EVM address + /// collateralToMoetFeePath: UniV3 fee tiers for each hop (length = path length - 1) + access(Configure) fun upsertMoetPreswapConfig( + composer: Type, + collateralVaultType: Type, + collateralToMoetAddressPath: [EVM.EVMAddress], + collateralToMoetFeePath: [UInt32] + ) { + pre { + composer == Type<@MorphoERC4626StrategyComposer>() + || composer == Type<@MoreERC4626StrategyComposer>(): + "composer must be MorphoERC4626StrategyComposer or MoreERC4626StrategyComposer" + collateralVaultType.isSubtype(of: Type<@{FungibleToken.Vault}>()): + "collateralVaultType must be a FungibleToken.Vault" + collateralToMoetAddressPath.length > 1: + "Path must have at least 2 elements" + collateralToMoetFeePath.length == collateralToMoetAddressPath.length - 1: + "Fee path length must equal address path length - 1" + } + FlowYieldVaultsStrategiesV2._setMoetPreswapConfig( + composer: composer, + collateral: collateralVaultType, + cfg: FlowYieldVaultsStrategiesV2.MoetPreswapConfig( + collateralToMoetAddressPath: collateralToMoetAddressPath, + collateralToMoetFeePath: collateralToMoetFeePath + ) + ) + } + access(Configure) fun purgeConfig() { self.configs = { Type<@MorphoERC4626StrategyComposer>(): { @@ -1561,6 +1963,7 @@ access(all) contract FlowYieldVaultsStrategiesV2 { } } FlowYieldVaultsStrategiesV2._purgeMoreERC4626Configs() + FlowYieldVaultsStrategiesV2.config["moetPreswapConfigs"] = {} as {Type: {Type: FlowYieldVaultsStrategiesV2.MoetPreswapConfig}} } } @@ -1665,6 +2068,12 @@ access(all) contract FlowYieldVaultsStrategiesV2 { FlowYieldVaultsStrategiesV2.config["debtToCollateralSwappers"] = partition } + access(contract) fun _removeDebtToCollateralSwapper(_ id: UInt64) { + var partition = FlowYieldVaultsStrategiesV2.config["debtToCollateralSwappers"] as! {UInt64: {DeFiActions.Swapper}}? ?? {} + partition.remove(key: id) + FlowYieldVaultsStrategiesV2.config["debtToCollateralSwappers"] = partition + } + // --- "collateralToDebtSwappers" partition --- // Stores a collateral→debt (collateral→PYUSD0→MOET) UniV3 swapper per FUSDEVStrategy uniqueID. // Used in FUSDEVStrategy.closePosition to pre-supplement the debt when yield tokens alone are @@ -1775,6 +2184,98 @@ access(all) contract FlowYieldVaultsStrategiesV2 { FlowYieldVaultsStrategiesV2.config["moreERC4626Configs"] = {} as {Type: {Type: {Type: MoreERC4626CollateralConfig}}} } + // --- "originalCollateralTypes" partition --- + // Stores the original (external) collateral type per strategy uniqueID when a MOET pre-swap + // is in effect. E.g. PYUSD0 when the position internally holds MOET. + + access(contract) view fun _getOriginalCollateralType(_ id: UInt64): Type? { + let partition = FlowYieldVaultsStrategiesV2.config["originalCollateralTypes"] as! {UInt64: Type}? ?? {} + return partition[id] + } + + access(contract) fun _setOriginalCollateralType(_ id: UInt64, _ t: Type) { + var partition = FlowYieldVaultsStrategiesV2.config["originalCollateralTypes"] as! {UInt64: Type}? ?? {} + partition[id] = t + FlowYieldVaultsStrategiesV2.config["originalCollateralTypes"] = partition + } + + access(contract) fun _removeOriginalCollateralType(_ id: UInt64) { + var partition = FlowYieldVaultsStrategiesV2.config["originalCollateralTypes"] as! {UInt64: Type}? ?? {} + partition.remove(key: id) + FlowYieldVaultsStrategiesV2.config["originalCollateralTypes"] = partition + } + + // --- "collateralPreSwappers" partition --- + // Stores a collateral→MOET swapper per strategy uniqueID. + // Used in deposit() to pre-swap incoming stablecoin collateral (e.g. PYUSD0) to MOET. + + access(contract) view fun _getCollateralPreSwapper(_ id: UInt64): {DeFiActions.Swapper}? { + let partition = FlowYieldVaultsStrategiesV2.config["collateralPreSwappers"] as! {UInt64: {DeFiActions.Swapper}}? ?? {} + return partition[id] + } + + access(contract) fun _setCollateralPreSwapper(_ id: UInt64, _ swapper: {DeFiActions.Swapper}) { + var partition = FlowYieldVaultsStrategiesV2.config["collateralPreSwappers"] as! {UInt64: {DeFiActions.Swapper}}? ?? {} + partition[id] = swapper + FlowYieldVaultsStrategiesV2.config["collateralPreSwappers"] = partition + } + + access(contract) fun _removeCollateralPreSwapper(_ id: UInt64) { + var partition = FlowYieldVaultsStrategiesV2.config["collateralPreSwappers"] as! {UInt64: {DeFiActions.Swapper}}? ?? {} + partition.remove(key: id) + FlowYieldVaultsStrategiesV2.config["collateralPreSwappers"] = partition + } + + // --- "moetToCollateralSwappers" partition --- + // Stores a MOET→original-collateral swapper per strategy uniqueID (FUSDEVStrategy and + // syWFLOWvStrategy). Built from the reversed MoetPreswapConfig path (same path in reverse). + // Used in closePosition to convert returned MOET collateral back to the original stablecoin + // (e.g. PYUSD0) for the no-debt path. The regular close path uses debtToCollateralSwappers. + + access(contract) view fun _getMoetToCollateralSwapper(_ id: UInt64): {DeFiActions.Swapper}? { + let partition = FlowYieldVaultsStrategiesV2.config["moetToCollateralSwappers"] as! {UInt64: {DeFiActions.Swapper}}? ?? {} + return partition[id] + } + + access(contract) fun _setMoetToCollateralSwapper(_ id: UInt64, _ swapper: {DeFiActions.Swapper}) { + var partition = FlowYieldVaultsStrategiesV2.config["moetToCollateralSwappers"] as! {UInt64: {DeFiActions.Swapper}}? ?? {} + partition[id] = swapper + FlowYieldVaultsStrategiesV2.config["moetToCollateralSwappers"] = partition + } + + access(contract) fun _removeMoetToCollateralSwapper(_ id: UInt64) { + var partition = FlowYieldVaultsStrategiesV2.config["moetToCollateralSwappers"] as! {UInt64: {DeFiActions.Swapper}}? ?? {} + partition.remove(key: id) + FlowYieldVaultsStrategiesV2.config["moetToCollateralSwappers"] = partition + } + + // --- "moetPreswapConfigs" partition --- + // Static admin config: keyed by composerType → collateralType → MoetPreswapConfig. + // Checked during createStrategy to determine whether a collateral needs MOET pre-swap. + + access(contract) view fun _getMoetPreswapConfig( + composer: Type, + collateral: Type + ): MoetPreswapConfig? { + let partition = FlowYieldVaultsStrategiesV2.config["moetPreswapConfigs"] + as! {Type: {Type: MoetPreswapConfig}}? ?? {} + let p = partition[composer] ?? {} + return p[collateral] + } + + access(contract) fun _setMoetPreswapConfig( + composer: Type, + collateral: Type, + cfg: MoetPreswapConfig + ) { + var partition = FlowYieldVaultsStrategiesV2.config["moetPreswapConfigs"] + as! {Type: {Type: MoetPreswapConfig}}? ?? {} + var p = partition[composer] ?? {} + p[collateral] = cfg + partition[composer] = p + FlowYieldVaultsStrategiesV2.config["moetPreswapConfigs"] = partition + } + init( univ3FactoryEVMAddress: String, univ3RouterEVMAddress: String, diff --git a/cadence/tests/FlowYieldVaultsStrategiesV2_syWFLOWv_test.cdc b/cadence/tests/FlowYieldVaultsStrategiesV2_syWFLOWv_test.cdc index 38ec38ca..70409eec 100644 --- a/cadence/tests/FlowYieldVaultsStrategiesV2_syWFLOWv_test.cdc +++ b/cadence/tests/FlowYieldVaultsStrategiesV2_syWFLOWv_test.cdc @@ -1,4 +1,4 @@ -#test_fork(network: "mainnet", height: 144698401) +#test_fork(network: "mainnet", height: nil) import Test From 3b7c5a750ccb6e21c1b7a1e8aedf3a293567b4be Mon Sep 17 00:00:00 2001 From: Alex <12097569+nialexsan@users.noreply.github.com> Date: Tue, 10 Mar 2026 17:37:21 -0400 Subject: [PATCH 19/27] fix PYUSD to MOET preswap logic --- .../contracts/FlowYieldVaultsStrategiesV2.cdc | 36 +++++++- ...ssert_flowalp_position_moet_collateral.cdc | 41 +++++++++ .../get_flowalp_position_ids.cdc | 10 +++ ...wYieldVaultsStrategiesV2_syWFLOWv_test.cdc | 69 +++++++++++++- .../transactions/seed_pool_moet_to_pyusd0.cdc | 89 +++++++++++++++++++ .../admin/upsert_moet_preswap_config.cdc | 49 ++++++++++ 6 files changed, 288 insertions(+), 6 deletions(-) create mode 100644 cadence/scripts/flow-yield-vaults/assert_flowalp_position_moet_collateral.cdc create mode 100644 cadence/scripts/flow-yield-vaults/get_flowalp_position_ids.cdc create mode 100644 cadence/tests/transactions/seed_pool_moet_to_pyusd0.cdc create mode 100644 cadence/transactions/flow-yield-vaults/admin/upsert_moet_preswap_config.cdc diff --git a/cadence/contracts/FlowYieldVaultsStrategiesV2.cdc b/cadence/contracts/FlowYieldVaultsStrategiesV2.cdc index 777714c4..1b09fa35 100644 --- a/cadence/contracts/FlowYieldVaultsStrategiesV2.cdc +++ b/cadence/contracts/FlowYieldVaultsStrategiesV2.cdc @@ -625,8 +625,30 @@ access(all) contract FlowYieldVaultsStrategiesV2 { } /// Withdraws up to the max amount, returning the withdrawn Vault. If the requested token type is unsupported, /// an empty Vault is returned. + /// + /// For the PYUSD0 pre-swap case: the internal source type is MOET but the external collateral + /// type is PYUSD0. We convert MOET→PYUSD0 via the moetToCollateralSwapper. access(FungibleToken.Withdraw) fun withdraw(maxAmount: UFix64, ofToken: Type): @{FungibleToken.Vault} { - if ofToken != self.source.getSourceType() { + let effectiveSourceType = self.source.getSourceType() + if ofToken != effectiveSourceType { + // For pre-swap case: ofToken is external collateral (e.g. PYUSD0), source is MOET. + if let id = self.uniqueID { + if let originalType = FlowYieldVaultsStrategiesV2._getOriginalCollateralType(id.id) { + if ofToken == originalType { + if let moetToOrigSwapper = FlowYieldVaultsStrategiesV2._getMoetToCollateralSwapper(id.id) { + // Quote MOET in to get maxAmount PYUSD0 out + let quote = moetToOrigSwapper.quoteIn(forDesired: maxAmount, reverse: false) + if quote.inAmount > 0.0 { + let moetVault <- self.source.withdrawAvailable(maxAmount: quote.inAmount) + if moetVault.balance > 0.0 { + return <- moetToOrigSwapper.swap(quote: nil, inVault: <-moetVault) + } + Burner.burn(<-moetVault) + } + } + } + } + } return <- DeFiActionsUtils.getEmptyVault(ofToken) } return <- self.source.withdrawAvailable(maxAmount: maxAmount) @@ -1608,13 +1630,19 @@ access(all) contract FlowYieldVaultsStrategiesV2 { balancerIO.autoBalancer.setSink(positionDebtSwapSinkPre, updateSinkID: true) balancerIO.autoBalancer.setSource(positionDebtSwapSourcePre, updateSourceID: true) - // FLOW→MOET: converts FLOW dust back to MOET (internal collateral) in closePosition + // FLOW→MOET: converts FLOW dust back to MOET (internal collateral) in closePosition. + // Path: FLOW → collateral (debtToCollateral) → MOET (pre-swap reversed hop). + // e.g. for PYUSD0: WFLOW→(fee 500)→PYUSD0→(fee 100)→MOET (2-hop). + var flowToMoetPath = collateralConfig.debtToCollateralUniV3AddressPath + flowToMoetPath.append(preswapCfg.collateralToMoetAddressPath[preswapCfg.collateralToMoetAddressPath.length - 1]) + var flowToMoetFees = collateralConfig.debtToCollateralUniV3FeePath + flowToMoetFees.append(preswapCfg.collateralToMoetFeePath[preswapCfg.collateralToMoetFeePath.length - 1]) let flowToMoet = UniswapV3SwapConnectors.Swapper( factoryAddress: FlowYieldVaultsStrategiesV2.univ3FactoryEVMAddress, routerAddress: FlowYieldVaultsStrategiesV2.univ3RouterEVMAddress, quoterAddress: FlowYieldVaultsStrategiesV2.univ3QuoterEVMAddress, - tokenPath: collateralConfig.debtToCollateralUniV3AddressPath, - feePath: collateralConfig.debtToCollateralUniV3FeePath, + tokenPath: flowToMoetPath, + feePath: flowToMoetFees, inVault: tokens.underlying4626AssetType, // FLOW outVault: tokens.moetTokenType, // MOET coaCapability: FlowYieldVaultsStrategiesV2._getCOACapability(), diff --git a/cadence/scripts/flow-yield-vaults/assert_flowalp_position_moet_collateral.cdc b/cadence/scripts/flow-yield-vaults/assert_flowalp_position_moet_collateral.cdc new file mode 100644 index 00000000..ba6693f4 --- /dev/null +++ b/cadence/scripts/flow-yield-vaults/assert_flowalp_position_moet_collateral.cdc @@ -0,0 +1,41 @@ +import "FlowALPv0" + +/// Verifies the PYUSD0→MOET pre-swap invariant for the FlowALP pool: +/// +/// 1. PYUSD0 is NOT a supported collateral token in FlowALP (so it cannot be deposited directly). +/// 2. MOET (the pool's default token) IS a supported collateral token. +/// +/// Together with a successful PYUSD0 vault creation test, this proves that the strategy +/// pre-swapped PYUSD0 → MOET before depositing into FlowALP — since FlowALP cannot receive +/// PYUSD0 directly. +/// +/// Returns a string starting with "OK:" on success or "FAIL:" on failure. +access(all) fun main(): String { + let pool = getAccount(0x6b00ff876c299c61) + .capabilities.borrow<&FlowALPv0.Pool>(FlowALPv0.PoolPublicPath) + ?? panic("Could not borrow FlowALP pool") + + let pyusd0TypeID = "A.1e4aa0b87d10b141.EVMVMBridgedToken_99af3eea856556646c98c8b9b2548fe815240750.Vault" + let supportedTokens = pool.getSupportedTokens() + + // Assert PYUSD0 is NOT supported (FlowALP cannot receive it as collateral) + for t in supportedTokens { + if t.identifier == pyusd0TypeID { + return "FAIL: PYUSD0 is listed as a supported FlowALP token — pre-swap may not be required" + } + } + + // Assert MOET (default token) IS supported + let defaultToken = pool.getDefaultToken() + var moetSupported = false + for t in supportedTokens { + if t == defaultToken { + moetSupported = true + } + } + if !moetSupported { + return "FAIL: MOET (pool default token) is not in the supported tokens list" + } + + return "OK: PYUSD0 is not a supported FlowALP collateral; MOET (".concat(defaultToken.identifier).concat(") is — pre-swap invariant confirmed") +} diff --git a/cadence/scripts/flow-yield-vaults/get_flowalp_position_ids.cdc b/cadence/scripts/flow-yield-vaults/get_flowalp_position_ids.cdc new file mode 100644 index 00000000..4fce8050 --- /dev/null +++ b/cadence/scripts/flow-yield-vaults/get_flowalp_position_ids.cdc @@ -0,0 +1,10 @@ +import "FlowALPv0" + +/// Returns all position IDs currently in the FlowALP pool at the hardcoded mainnet address. +/// Used in fork tests to snapshot existing positions before a test creates new ones. +access(all) fun main(): [UInt64] { + let pool = getAccount(0x6b00ff876c299c61) + .capabilities.borrow<&FlowALPv0.Pool>(FlowALPv0.PoolPublicPath) + ?? panic("Could not borrow FlowALP pool") + return pool.getPositionIDs() +} diff --git a/cadence/tests/FlowYieldVaultsStrategiesV2_syWFLOWv_test.cdc b/cadence/tests/FlowYieldVaultsStrategiesV2_syWFLOWv_test.cdc index 70409eec..68756367 100644 --- a/cadence/tests/FlowYieldVaultsStrategiesV2_syWFLOWv_test.cdc +++ b/cadence/tests/FlowYieldVaultsStrategiesV2_syWFLOWv_test.cdc @@ -4,6 +4,7 @@ import Test import "EVM" import "FlowToken" +import "FlowALPv0" import "FlowYieldVaults" import "FlowYieldVaultsClosedBeta" @@ -63,6 +64,7 @@ access(all) let wethVaultIdentifier = "A.1e4aa0b87d10b141.EVMVMBridgedToken_2f access(all) let syWFLOWvEVMAddress = "0xCBf9a7753F9D2d0e8141ebB36d99f87AcEf98597" access(all) let wflowEVMAddress = "0xd3bF53DAC106A0290B0483EcBC89d40FcC961f3e" access(all) let pyusd0EVMAddress = "0x99aF3EeA856556646C98c8B9b2548Fe815240750" +access(all) let moetEVMAddress = "0x213979bb8a9a86966999b3aa797c1fcf3b967ae2" access(all) let wbtcEVMAddress = "0x717DAE2BaF7656BE9a9B01deE31d571a9d4c9579" access(all) let wethEVMAddress = "0x2F6F07CDcf3588944Bf4C42aC74ff24bF56e7590" @@ -72,6 +74,7 @@ access(all) var pyusd0VaultID: UInt64 = 0 access(all) var wbtcVaultID: UInt64 = 0 access(all) var wethVaultID: UInt64 = 0 + /* --- Helpers --- */ access(all) @@ -232,6 +235,22 @@ access(all) fun setup() { ) Test.expect(result, Test.beSucceeded()) + // Configure PYUSD0 → MOET pre-swap for MoreERC4626StrategyComposer. + // FlowALP only accepts MOET as its stablecoin collateral; PYUSD0 must be swapped first. + // PYUSD0/MOET is a stablecoin pair — fee tier 100 (0.01%). + log("Configuring MOET pre-swap: MoreERC4626StrategyComposer + PYUSD0 → MOET (fee 100)...") + result = _executeTransactionFile( + "../transactions/flow-yield-vaults/admin/upsert_moet_preswap_config.cdc", + [ + composerIdentifier, + pyusd0VaultIdentifier, + [pyusd0EVMAddress, moetEVMAddress] as [String], // PYUSD0 → MOET + [100 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( @@ -313,6 +332,27 @@ access(all) fun setup() { ) Test.expect(result, Test.beSucceeded()) + // Seed the PYUSD0/MOET pool with MOET so the PYUSD0→MOET pre-swap has liquidity. + // + // Background: the mainnet PYUSD0/MOET pool at fee 100 accumulates PYUSD0 over time because + // strategies sell MOET→PYUSD0. Before testing PYUSD0→MOET pre-swap we restore MOET reserves + // by swapping MOET→PYUSD0. The wbtcUser now has MOET from the auto-borrowed reserve position. + log("Seeding PYUSD0/MOET pool: swapping 50 MOET → PYUSD0 via UniV3 fee 100...") + result = _executeTransactionFile( + "transactions/seed_pool_moet_to_pyusd0.cdc", + [ + "0xca6d7Bb03334bBf135902e1d919a5feccb461632", // UniV3 factory + "0xeEDC6Ff75e1b10B903D9013c358e446a73d35341", // UniV3 router + "0x370A8DF17742867a44e56223EC20D82092242C85", // UniV3 quoter + moetEVMAddress, + pyusd0EVMAddress, + 100 as UInt32, + 50.0 as UFix64 + ], + [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...") @@ -349,10 +389,10 @@ access(all) fun setup() { ========================================================= */ access(all) fun testCreateSyWFLOWvYieldVault_PYUSD0() { - log("Creating syWFLOWvStrategy yield vault with 1.0 PYUSD0...") + log("Creating syWFLOWvStrategy yield vault with 2.0 PYUSD0...") let result = _executeTransactionFile( "../transactions/flow-yield-vaults/create_yield_vault.cdc", - [syWFLOWvStrategyIdentifier, pyusd0VaultIdentifier, 1.0], + [syWFLOWvStrategyIdentifier, pyusd0VaultIdentifier, 2.0], [pyusd0User] ) Test.expect(result, Test.beSucceeded()) @@ -365,6 +405,31 @@ access(all) fun testCreateSyWFLOWvYieldVault_PYUSD0() { let balance = bal.returnValue! as! UFix64? Test.assert(balance != nil && balance! > 0.0, message: "Expected positive balance after create (PYUSD0)") log("PYUSD0 vault balance after create: ".concat(balance!.toString())) + + // Verify the PYUSD0→MOET pre-swap happened by checking FlowALPv0.Deposited events: + // - There must be a Deposited event with vaultType = MOET (pre-swapped collateral) + // - There must be NO Deposited event with vaultType = PYUSD0 (should never reach FlowALP) + let depositedEvents = Test.eventsOfType(Type()) + log("FlowALPv0.Deposited events: ".concat(depositedEvents.length.toString())) + + 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=".concat(ev.vaultType.identifier).concat(" amount=").concat(ev.amount.toString())) + if ev.vaultType.identifier == moetTypeID { + foundMoetDeposit = true + } + if ev.vaultType.identifier == pyusd0VaultIdentifier { + foundPyusd0Deposit = true + } + } + Test.assert(foundMoetDeposit, + message: "Expected FlowALPv0.Deposited event with MOET — pre-swap did not deposit MOET into FlowALP") + Test.assert(!foundPyusd0Deposit, + message: "Unexpected FlowALPv0.Deposited event with PYUSD0 — pre-swap was bypassed") + log("Confirmed: FlowALP received MOET as collateral (PYUSD0 was pre-swapped before FlowALP deposit)") } access(all) fun testDepositToSyWFLOWvYieldVault_PYUSD0() { diff --git a/cadence/tests/transactions/seed_pool_moet_to_pyusd0.cdc b/cadence/tests/transactions/seed_pool_moet_to_pyusd0.cdc new file mode 100644 index 00000000..9fc8e8e9 --- /dev/null +++ b/cadence/tests/transactions/seed_pool_moet_to_pyusd0.cdc @@ -0,0 +1,89 @@ +import "FungibleToken" +import "FungibleTokenMetadataViews" +import "EVM" +import "MOET" +import "UniswapV3SwapConnectors" +import "FlowEVMBridgeConfig" + +/// Swap MOET → PYUSD0 via UniV3 to seed the PYUSD0/MOET pool with MOET. +/// +/// Purpose: the PYUSD0/MOET pool on mainnet can become MOET-depleted (strategies sell MOET +/// for PYUSD0). Before testing PYUSD0→MOET pre-swap, this transaction restores MOET +/// liquidity so the reverse swap is viable. +/// +/// The signer must hold MOET in their MOET vault (e.g. from creating a FlowALP position). +/// PYUSD0 received from the swap is deposited into the signer's PYUSD0 vault (set up if absent). +/// +/// @param factoryAddr: UniswapV3 factory EVM address (hex, with 0x prefix) +/// @param routerAddr: UniswapV3 router EVM address +/// @param quoterAddr: UniswapV3 quoter EVM address +/// @param moetEvmAddr: MOET EVM address (e.g. "0x213979bb8a9a86966999b3aa797c1fcf3b967ae2") +/// @param pyusd0EvmAddr: PYUSD0 EVM address +/// @param fee: UniV3 pool fee tier (100 = 0.01%) +/// @param moetAmount: Amount of MOET to swap + +transaction( + factoryAddr: String, + routerAddr: String, + quoterAddr: String, + moetEvmAddr: String, + pyusd0EvmAddr: String, + fee: UInt32, + moetAmount: UFix64 +) { + prepare(signer: auth(Storage, BorrowValue, IssueStorageCapabilityController, SaveValue, PublishCapability, UnpublishCapability) &Account) { + let coaCap = signer.capabilities.storage.issue(/storage/evm) + + let moetEVM = EVM.addressFromString(moetEvmAddr) + let pyusd0EVM = EVM.addressFromString(pyusd0EvmAddr) + + let moetType = Type<@MOET.Vault>() + let pyusd0Type = FlowEVMBridgeConfig.getTypeAssociated(with: pyusd0EVM) + ?? panic("PYUSD0 EVM address not registered in bridge config: ".concat(pyusd0EvmAddr)) + + let swapper = UniswapV3SwapConnectors.Swapper( + factoryAddress: EVM.addressFromString(factoryAddr), + routerAddress: EVM.addressFromString(routerAddr), + quoterAddress: EVM.addressFromString(quoterAddr), + tokenPath: [moetEVM, pyusd0EVM], + feePath: [fee], + inVault: moetType, + outVault: pyusd0Type, + coaCapability: coaCap, + uniqueID: nil + ) + + let moetProvider = signer.storage.borrow( + from: MOET.VaultStoragePath + ) ?? panic("No MOET vault found in signer storage at ".concat(MOET.VaultStoragePath.toString()).concat(" — ensure the signer created a FlowALP position")) + + let inVault <- moetProvider.withdraw(amount: moetAmount) + let outVault <- swapper.swap(quote: nil, inVault: <-inVault) + log("Seeded pool: swapped ".concat(moetAmount.toString()).concat(" MOET → ").concat(outVault.balance.toString()).concat(" PYUSD0")) + + // Deposit PYUSD0 into signer's storage (set up vault if missing). + let pyusd0CompType = CompositeType(pyusd0Type.identifier) + ?? panic("Cannot construct CompositeType for PYUSD0: ".concat(pyusd0Type.identifier)) + let pyusd0Contract = getAccount(pyusd0CompType.address!).contracts.borrow<&{FungibleToken}>(name: pyusd0CompType.contractName!) + ?? panic("Cannot borrow FungibleToken contract for PYUSD0") + let pyusd0VaultData = pyusd0Contract.resolveContractView( + resourceType: pyusd0CompType, + viewType: Type() + ) as? FungibleTokenMetadataViews.FTVaultData + ?? panic("Cannot resolve FTVaultData for PYUSD0") + + if signer.storage.borrow<&{FungibleToken.Vault}>(from: pyusd0VaultData.storagePath) == nil { + signer.storage.save(<-pyusd0VaultData.createEmptyVault(), to: pyusd0VaultData.storagePath) + signer.capabilities.unpublish(pyusd0VaultData.receiverPath) + signer.capabilities.unpublish(pyusd0VaultData.metadataPath) + let receiverCap = signer.capabilities.storage.issue<&{FungibleToken.Vault}>(pyusd0VaultData.storagePath) + let metadataCap = signer.capabilities.storage.issue<&{FungibleToken.Vault}>(pyusd0VaultData.storagePath) + signer.capabilities.publish(receiverCap, at: pyusd0VaultData.receiverPath) + signer.capabilities.publish(metadataCap, at: pyusd0VaultData.metadataPath) + } + + let receiver = signer.storage.borrow<&{FungibleToken.Receiver}>(from: pyusd0VaultData.storagePath) + ?? panic("Cannot borrow PYUSD0 vault receiver") + receiver.deposit(from: <-outVault) + } +} diff --git a/cadence/transactions/flow-yield-vaults/admin/upsert_moet_preswap_config.cdc b/cadence/transactions/flow-yield-vaults/admin/upsert_moet_preswap_config.cdc new file mode 100644 index 00000000..858ac696 --- /dev/null +++ b/cadence/transactions/flow-yield-vaults/admin/upsert_moet_preswap_config.cdc @@ -0,0 +1,49 @@ +import "FlowYieldVaultsStrategiesV2" +import "FlowYieldVaults" +import "EVM" + +/// Configures a stablecoin collateral type to use MOET pre-swap for a given StrategyComposer. +/// Required for stablecoins (e.g. PYUSD0) that FlowALP does not support as direct collateral. +/// +/// Parameters: +/// composerTypeIdentifier: e.g. "A.xxx.FlowYieldVaultsStrategiesV2.MorphoERC4626StrategyComposer" +/// collateralVaultTypeIdentifier: e.g. "A.yyy.EVMVMBridgedToken_99af....Vault" +/// collateralToMoetAddressPath: array of EVM address hex strings, collateral→MOET path +/// e.g. ["0x99af...", "0x02d3..."] (1-hop) or 3+ for multi-hop +/// collateralToMoetFeePath: array of UInt32 fee tiers, one per hop +/// e.g. [100] for 0.01%, [3000] for 0.3% +transaction( + composerTypeIdentifier: String, + collateralVaultTypeIdentifier: String, + collateralToMoetAddressPath: [String], + collateralToMoetFeePath: [UInt32] +) { + let issuer: auth(FlowYieldVaultsStrategiesV2.Configure) &FlowYieldVaultsStrategiesV2.StrategyComposerIssuer + + prepare(admin: auth(Storage) &Account) { + self.issuer = admin.storage.borrow( + from: FlowYieldVaultsStrategiesV2.IssuerStoragePath + ) ?? panic("Could not borrow StrategyComposerIssuer from \(FlowYieldVaultsStrategiesV2.IssuerStoragePath)") + } + + execute { + let composerType = CompositeType(composerTypeIdentifier) + ?? panic("Invalid composer type identifier: \(composerTypeIdentifier)") + let collateralVaultType = CompositeType(collateralVaultTypeIdentifier) + ?? panic("Invalid collateral vault type identifier: \(collateralVaultTypeIdentifier)") + + var evmPath: [EVM.EVMAddress] = [] + for addr in collateralToMoetAddressPath { + evmPath.append(EVM.addressFromString(addr)) + } + + self.issuer.upsertMoetPreswapConfig( + composer: composerType, + collateralVaultType: collateralVaultType, + collateralToMoetAddressPath: evmPath, + collateralToMoetFeePath: collateralToMoetFeePath + ) + + log("Configured MOET pre-swap for composer \(composerTypeIdentifier) collateral \(collateralVaultTypeIdentifier)") + } +} From 37e040a443ea00ed764d1d6922e3d70307075153 Mon Sep 17 00:00:00 2001 From: Alex <12097569+nialexsan@users.noreply.github.com> Date: Tue, 10 Mar 2026 22:23:47 -0400 Subject: [PATCH 20/27] testnet setup --- .../contracts/FlowYieldVaultsStrategiesV2.cdc | 2 +- .../admin/remove_strategy_composer.cdc | 30 ++ flow.json | 275 +++++++++++++----- local/setup_testnet.sh | 128 +++++++- local/setup_testnet_fork.sh | 121 ++++++++ 5 files changed, 479 insertions(+), 77 deletions(-) create mode 100644 cadence/transactions/flow-yield-vaults/admin/remove_strategy_composer.cdc create mode 100755 local/setup_testnet_fork.sh diff --git a/cadence/contracts/FlowYieldVaultsStrategiesV2.cdc b/cadence/contracts/FlowYieldVaultsStrategiesV2.cdc index 1b09fa35..060b4301 100644 --- a/cadence/contracts/FlowYieldVaultsStrategiesV2.cdc +++ b/cadence/contracts/FlowYieldVaultsStrategiesV2.cdc @@ -1441,7 +1441,7 @@ access(all) contract FlowYieldVaultsStrategiesV2 { // collateral EVM address = last element of yieldToCollateral path // underlying (PYUSD0) EVM address = second element of yieldToCollateral path - assert(yieldToCollPath.length >= 3, message: "yieldToCollateral path must have at least 3 elements") + assert(yieldToCollPath.length >= 2, message: "yieldToCollateral path must have at least 2 elements") let collateralEVMAddress = yieldToCollPath[yieldToCollPath.length - 1] let underlyingEVMAddress = tokens.underlying4626AssetEVMAddress 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/flow.json b/flow.json index 9c9dbc33..8c351dbc 100644 --- a/flow.json +++ b/flow.json @@ -19,7 +19,8 @@ "mainnet": "e36ef556b8b5d955", "mainnet-fork": "e36ef556b8b5d955", "testing": "0000000000000007", - "testnet": "bb76ea2f8aad74a0" + "testnet": "bb76ea2f8aad74a0", + "testnet-fork": "bb76ea2f8aad74a0" } }, "DeFiActions": { @@ -29,7 +30,8 @@ "mainnet": "6d888f175c158410", "mainnet-fork": "6d888f175c158410", "testing": "0000000000000007", - "testnet": "0b11b1848a8aa2c0" + "testnet": "0b11b1848a8aa2c0", + "testnet-fork": "0b11b1848a8aa2c0" } }, "DeFiActionsUtils": { @@ -39,7 +41,8 @@ "mainnet": "6d888f175c158410", "mainnet-fork": "6d888f175c158410", "testing": "0000000000000007", - "testnet": "0b11b1848a8aa2c0" + "testnet": "0b11b1848a8aa2c0", + "testnet-fork": "0b11b1848a8aa2c0" } }, "DummyConnectors": { @@ -47,7 +50,8 @@ "aliases": { "emulator": "045a1763c93006ca", "testing": "0000000000000008", - "testnet": "d2580caf2ef07c2f" + "testnet": "d2580caf2ef07c2f", + "testnet-fork": "d2580caf2ef07c2f" } }, "ERC4626PriceOracles": { @@ -57,7 +61,8 @@ "mainnet": "04f5ae6bef48c1fc", "mainnet-fork": "04f5ae6bef48c1fc", "testing": "0000000000000009", - "testnet": "7014dcffa1f14186" + "testnet": "7014dcffa1f14186", + "testnet-fork": "7014dcffa1f14186" } }, "ERC4626SinkConnectors": { @@ -67,7 +72,8 @@ "mainnet": "04f5ae6bef48c1fc", "mainnet-fork": "04f5ae6bef48c1fc", "testing": "0000000000000009", - "testnet": "7014dcffa1f14186" + "testnet": "7014dcffa1f14186", + "testnet-fork": "7014dcffa1f14186" } }, "ERC4626SwapConnectors": { @@ -77,7 +83,8 @@ "mainnet": "04f5ae6bef48c1fc", "mainnet-fork": "04f5ae6bef48c1fc", "testing": "0000000000000009", - "testnet": "7014dcffa1f14186" + "testnet": "7014dcffa1f14186", + "testnet-fork": "7014dcffa1f14186" } }, "ERC4626Utils": { @@ -87,7 +94,8 @@ "mainnet": "04f5ae6bef48c1fc", "mainnet-fork": "04f5ae6bef48c1fc", "testing": "0000000000000009", - "testnet": "7014dcffa1f14186" + "testnet": "7014dcffa1f14186", + "testnet-fork": "7014dcffa1f14186" } }, "EVMAbiHelpers": { @@ -97,7 +105,8 @@ "mainnet": "a7825d405ac89518", "mainnet-fork": "a7825d405ac89518", "testing": "0000000000000007", - "testnet": "3ebb7d2595e97cd2" + "testnet": "3ebb7d2595e97cd2", + "testnet-fork": "3ebb7d2595e97cd2" } }, "EVMAmountUtils": { @@ -107,7 +116,8 @@ "mainnet": "43c9e8bfec507db4", "mainnet-fork": "43c9e8bfec507db4", "testing": "0000000000000009", - "testnet": "67402f29666f7b29" + "testnet": "67402f29666f7b29", + "testnet-fork": "67402f29666f7b29" } }, "EVMTokenConnectors": { @@ -117,7 +127,8 @@ "mainnet": "1a771b21fcceadc2", "mainnet-fork": "1a771b21fcceadc2", "testing": "0000000000000009", - "testnet": "b88ba0e976146cd1" + "testnet": "b88ba0e976146cd1", + "testnet-fork": "b88ba0e976146cd1" } }, "FlowALPMath": { @@ -127,7 +138,8 @@ "mainnet": "6b00ff876c299c61", "mainnet-fork": "6b00ff876c299c61", "testing": "0000000000000007", - "testnet": "426f0458ced60037" + "testnet": "426f0458ced60037", + "testnet-fork": "426f0458ced60037" } }, "FlowALPv0": { @@ -137,7 +149,8 @@ "mainnet": "6b00ff876c299c61", "mainnet-fork": "6b00ff876c299c61", "testing": "0000000000000008", - "testnet": "426f0458ced60037" + "testnet": "426f0458ced60037", + "testnet-fork": "426f0458ced60037" } }, "FlowYieldVaults": { @@ -147,7 +160,8 @@ "mainnet": "b1d63873c3cc9f79", "mainnet-fork": "b1d63873c3cc9f79", "testing": "0000000000000009", - "testnet": "d2580caf2ef07c2f" + "testnet": "d2580caf2ef07c2f", + "testnet-fork": "d2580caf2ef07c2f" } }, "FlowYieldVaultsAutoBalancers": { @@ -157,7 +171,8 @@ "mainnet": "b1d63873c3cc9f79", "mainnet-fork": "b1d63873c3cc9f79", "testing": "0000000000000009", - "testnet": "d2580caf2ef07c2f" + "testnet": "d2580caf2ef07c2f", + "testnet-fork": "d2580caf2ef07c2f" } }, "FlowYieldVaultsClosedBeta": { @@ -167,7 +182,8 @@ "mainnet": "b1d63873c3cc9f79", "mainnet-fork": "b1d63873c3cc9f79", "testing": "0000000000000009", - "testnet": "d2580caf2ef07c2f" + "testnet": "d2580caf2ef07c2f", + "testnet-fork": "d2580caf2ef07c2f" } }, "FlowYieldVaultsSchedulerRegistry": { @@ -177,7 +193,8 @@ "mainnet": "b1d63873c3cc9f79", "mainnet-fork": "b1d63873c3cc9f79", "testing": "0000000000000009", - "testnet": "d2580caf2ef07c2f" + "testnet": "d2580caf2ef07c2f", + "testnet-fork": "d2580caf2ef07c2f" } }, "FlowYieldVaultsSchedulerV1": { @@ -187,7 +204,8 @@ "mainnet": "b1d63873c3cc9f79", "mainnet-fork": "b1d63873c3cc9f79", "testing": "0000000000000009", - "testnet": "d2580caf2ef07c2f" + "testnet": "d2580caf2ef07c2f", + "testnet-fork": "d2580caf2ef07c2f" } }, "FlowYieldVaultsStrategiesV2": { @@ -197,7 +215,8 @@ "mainnet": "b1d63873c3cc9f79", "mainnet-fork": "b1d63873c3cc9f79", "testing": "0000000000000009", - "testnet": "d2580caf2ef07c2f" + "testnet": "d2580caf2ef07c2f", + "testnet-fork": "d2580caf2ef07c2f" } }, "FungibleTokenConnectors": { @@ -207,7 +226,8 @@ "mainnet": "0c237e1265caa7a3", "mainnet-fork": "0c237e1265caa7a3", "testing": "0000000000000007", - "testnet": "4cd02f8de4122c84" + "testnet": "4cd02f8de4122c84", + "testnet-fork": "4cd02f8de4122c84" } }, "MOET": { @@ -217,7 +237,8 @@ "mainnet": "6b00ff876c299c61", "mainnet-fork": "6b00ff876c299c61", "testing": "0000000000000008", - "testnet": "426f0458ced60037" + "testnet": "426f0458ced60037", + "testnet-fork": "426f0458ced60037" } }, "MockDexSwapper": { @@ -234,7 +255,8 @@ "mainnet": "b1d63873c3cc9f79", "mainnet-fork": "b1d63873c3cc9f79", "testing": "0000000000000009", - "testnet": "d2580caf2ef07c2f" + "testnet": "d2580caf2ef07c2f", + "testnet-fork": "d2580caf2ef07c2f" } }, "MockStrategies": { @@ -244,7 +266,8 @@ "mainnet": "b1d63873c3cc9f79", "mainnet-fork": "b1d63873c3cc9f79", "testing": "0000000000000009", - "testnet": "d2580caf2ef07c2f" + "testnet": "d2580caf2ef07c2f", + "testnet-fork": "d2580caf2ef07c2f" } }, "MockStrategy": { @@ -252,7 +275,8 @@ "aliases": { "emulator": "045a1763c93006ca", "testing": "0000000000000009", - "testnet": "d2580caf2ef07c2f" + "testnet": "d2580caf2ef07c2f", + "testnet-fork": "d2580caf2ef07c2f" } }, "MockSwapper": { @@ -262,7 +286,8 @@ "mainnet": "b1d63873c3cc9f79", "mainnet-fork": "b1d63873c3cc9f79", "testing": "0000000000000009", - "testnet": "d2580caf2ef07c2f" + "testnet": "d2580caf2ef07c2f", + "testnet-fork": "d2580caf2ef07c2f" } }, "MorphoERC4626SinkConnectors": { @@ -272,7 +297,8 @@ "mainnet": "251032a66e9700ef", "mainnet-fork": "251032a66e9700ef", "testing": "0000000000000009", - "testnet": "71144a1aff6b7148" + "testnet": "71144a1aff6b7148", + "testnet-fork": "71144a1aff6b7148" } }, "MorphoERC4626SwapConnectors": { @@ -282,7 +308,8 @@ "mainnet": "251032a66e9700ef", "mainnet-fork": "251032a66e9700ef", "testing": "0000000000000009", - "testnet": "71144a1aff6b7148" + "testnet": "71144a1aff6b7148", + "testnet-fork": "71144a1aff6b7148" } }, "PMStrategiesV1": { @@ -292,7 +319,8 @@ "mainnet": "b1d63873c3cc9f79", "mainnet-fork": "b1d63873c3cc9f79", "testing": "0000000000000009", - "testnet": "d2580caf2ef07c2f" + "testnet": "d2580caf2ef07c2f", + "testnet-fork": "d2580caf2ef07c2f" } }, "SwapConnectors": { @@ -302,7 +330,8 @@ "mainnet": "e1a479f0cb911df9", "mainnet-fork": "e1a479f0cb911df9", "testing": "0000000000000007", - "testnet": "ad228f1c13a97ec1" + "testnet": "ad228f1c13a97ec1", + "testnet-fork": "ad228f1c13a97ec1" } }, "UniswapV3SwapConnectors": { @@ -312,7 +341,8 @@ "mainnet": "a7825d405ac89518", "mainnet-fork": "a7825d405ac89518", "testing": "0000000000000007", - "testnet": "3ebb7d2595e97cd2" + "testnet": "3ebb7d2595e97cd2", + "testnet-fork": "3ebb7d2595e97cd2" } }, "YieldToken": { @@ -322,7 +352,8 @@ "mainnet": "b1d63873c3cc9f79", "mainnet-fork": "b1d63873c3cc9f79", "testing": "0000000000000010", - "testnet": "d2580caf2ef07c2f" + "testnet": "d2580caf2ef07c2f", + "testnet-fork": "d2580caf2ef07c2f" } } }, @@ -347,7 +378,8 @@ "mainnet": "6801a6222ebf784a", "mainnet-fork": "6801a6222ebf784a", "testing": "0000000000000007", - "testnet": "9fb6606c300b5051" + "testnet": "9fb6606c300b5051", + "testnet-fork": "9fb6606c300b5051" } }, "Burner": { @@ -358,7 +390,8 @@ "emulator": "f8d6e0586b0a20c7", "mainnet": "f233dcee88fe0abe", "mainnet-fork": "f233dcee88fe0abe", - "testnet": "9a0766d93b6608b7" + "testnet": "9a0766d93b6608b7", + "testnet-fork": "9a0766d93b6608b7" } }, "CrossVMMetadataViews": { @@ -369,7 +402,8 @@ "emulator": "f8d6e0586b0a20c7", "mainnet": "1d7e57aa55817448", "mainnet-fork": "1d7e57aa55817448", - "testnet": "631e88ae7f1d7c20" + "testnet": "631e88ae7f1d7c20", + "testnet-fork": "631e88ae7f1d7c20" } }, "CrossVMNFT": { @@ -392,7 +426,8 @@ "mainnet": "1e4aa0b87d10b141", "mainnet-fork": "1e4aa0b87d10b141", "testing": "0000000000000001", - "testnet": "dfc20aee650fcbdf" + "testnet": "dfc20aee650fcbdf", + "testnet-fork": "dfc20aee650fcbdf" } }, "EVM": { @@ -403,7 +438,8 @@ "emulator": "f8d6e0586b0a20c7", "mainnet": "e467b9dd11fa00df", "mainnet-fork": "e467b9dd11fa00df", - "testnet": "8c5303eaa26202d6" + "testnet": "8c5303eaa26202d6", + "testnet-fork": "8c5303eaa26202d6" } }, "FlowEVMBridge": { @@ -415,7 +451,8 @@ "mainnet": "1e4aa0b87d10b141", "mainnet-fork": "1e4aa0b87d10b141", "testing": "0000000000000001", - "testnet": "dfc20aee650fcbdf" + "testnet": "dfc20aee650fcbdf", + "testnet-fork": "dfc20aee650fcbdf" } }, "FlowEVMBridgeAccessor": { @@ -427,7 +464,8 @@ "mainnet": "1e4aa0b87d10b141", "mainnet-fork": "1e4aa0b87d10b141", "testing": "0000000000000001", - "testnet": "dfc20aee650fcbdf" + "testnet": "dfc20aee650fcbdf", + "testnet-fork": "dfc20aee650fcbdf" } }, "FlowEVMBridgeConfig": { @@ -439,7 +477,8 @@ "mainnet": "1e4aa0b87d10b141", "mainnet-fork": "1e4aa0b87d10b141", "testing": "0000000000000001", - "testnet": "dfc20aee650fcbdf" + "testnet": "dfc20aee650fcbdf", + "testnet-fork": "dfc20aee650fcbdf" } }, "FlowEVMBridgeCustomAssociationTypes": { @@ -451,7 +490,8 @@ "mainnet": "1e4aa0b87d10b141", "mainnet-fork": "1e4aa0b87d10b141", "testing": "0000000000000001", - "testnet": "dfc20aee650fcbdf" + "testnet": "dfc20aee650fcbdf", + "testnet-fork": "dfc20aee650fcbdf" } }, "FlowEVMBridgeCustomAssociations": { @@ -463,7 +503,8 @@ "mainnet": "1e4aa0b87d10b141", "mainnet-fork": "1e4aa0b87d10b141", "testing": "0000000000000001", - "testnet": "dfc20aee650fcbdf" + "testnet": "dfc20aee650fcbdf", + "testnet-fork": "dfc20aee650fcbdf" } }, "FlowEVMBridgeHandlerInterfaces": { @@ -475,7 +516,8 @@ "mainnet": "1e4aa0b87d10b141", "mainnet-fork": "1e4aa0b87d10b141", "testing": "0000000000000001", - "testnet": "dfc20aee650fcbdf" + "testnet": "dfc20aee650fcbdf", + "testnet-fork": "dfc20aee650fcbdf" } }, "FlowEVMBridgeHandlers": { @@ -487,7 +529,8 @@ "mainnet": "1e4aa0b87d10b141", "mainnet-fork": "1e4aa0b87d10b141", "testing": "0000000000000001", - "testnet": "dfc20aee650fcbdf" + "testnet": "dfc20aee650fcbdf", + "testnet-fork": "dfc20aee650fcbdf" } }, "FlowEVMBridgeNFTEscrow": { @@ -499,7 +542,8 @@ "mainnet": "1e4aa0b87d10b141", "mainnet-fork": "1e4aa0b87d10b141", "testing": "0000000000000001", - "testnet": "dfc20aee650fcbdf" + "testnet": "dfc20aee650fcbdf", + "testnet-fork": "dfc20aee650fcbdf" } }, "FlowEVMBridgeResolver": { @@ -511,7 +555,8 @@ "mainnet": "1e4aa0b87d10b141", "mainnet-fork": "1e4aa0b87d10b141", "testing": "0000000000000001", - "testnet": "dfc20aee650fcbdf" + "testnet": "dfc20aee650fcbdf", + "testnet-fork": "dfc20aee650fcbdf" } }, "FlowEVMBridgeTemplates": { @@ -523,7 +568,8 @@ "mainnet": "1e4aa0b87d10b141", "mainnet-fork": "1e4aa0b87d10b141", "testing": "0000000000000001", - "testnet": "dfc20aee650fcbdf" + "testnet": "dfc20aee650fcbdf", + "testnet-fork": "dfc20aee650fcbdf" } }, "FlowEVMBridgeTokenEscrow": { @@ -535,7 +581,8 @@ "mainnet": "1e4aa0b87d10b141", "mainnet-fork": "1e4aa0b87d10b141", "testing": "0000000000000001", - "testnet": "dfc20aee650fcbdf" + "testnet": "dfc20aee650fcbdf", + "testnet-fork": "dfc20aee650fcbdf" } }, "FlowEVMBridgeUtils": { @@ -547,7 +594,8 @@ "mainnet": "1e4aa0b87d10b141", "mainnet-fork": "1e4aa0b87d10b141", "testing": "0000000000000001", - "testnet": "dfc20aee650fcbdf" + "testnet": "dfc20aee650fcbdf", + "testnet-fork": "dfc20aee650fcbdf" } }, "FlowFees": { @@ -558,7 +606,8 @@ "emulator": "e5a8b7f23e8b548f", "mainnet": "f919ee77447b7497", "mainnet-fork": "f919ee77447b7497", - "testnet": "912d5440f7e3769e" + "testnet": "912d5440f7e3769e", + "testnet-fork": "912d5440f7e3769e" } }, "FlowStorageFees": { @@ -569,7 +618,8 @@ "emulator": "f8d6e0586b0a20c7", "mainnet": "e467b9dd11fa00df", "mainnet-fork": "e467b9dd11fa00df", - "testnet": "8c5303eaa26202d6" + "testnet": "8c5303eaa26202d6", + "testnet-fork": "8c5303eaa26202d6" } }, "FlowToken": { @@ -580,7 +630,8 @@ "emulator": "0ae53cb6e3f42a79", "mainnet": "1654653399040a61", "mainnet-fork": "1654653399040a61", - "testnet": "7e60df042a9c0868" + "testnet": "7e60df042a9c0868", + "testnet-fork": "7e60df042a9c0868" } }, "FlowTransactionScheduler": { @@ -591,7 +642,8 @@ "emulator": "f8d6e0586b0a20c7", "mainnet": "e467b9dd11fa00df", "mainnet-fork": "e467b9dd11fa00df", - "testnet": "8c5303eaa26202d6" + "testnet": "8c5303eaa26202d6", + "testnet-fork": "8c5303eaa26202d6" } }, "FlowTransactionSchedulerUtils": { @@ -602,7 +654,8 @@ "emulator": "f8d6e0586b0a20c7", "mainnet": "e467b9dd11fa00df", "mainnet-fork": "e467b9dd11fa00df", - "testnet": "8c5303eaa26202d6" + "testnet": "8c5303eaa26202d6", + "testnet-fork": "8c5303eaa26202d6" } }, "FungibleToken": { @@ -613,7 +666,8 @@ "emulator": "ee82856bf20e2aa6", "mainnet": "f233dcee88fe0abe", "mainnet-fork": "f233dcee88fe0abe", - "testnet": "9a0766d93b6608b7" + "testnet": "9a0766d93b6608b7", + "testnet-fork": "9a0766d93b6608b7" } }, "FungibleTokenMetadataViews": { @@ -624,7 +678,8 @@ "emulator": "ee82856bf20e2aa6", "mainnet": "f233dcee88fe0abe", "mainnet-fork": "f233dcee88fe0abe", - "testnet": "9a0766d93b6608b7" + "testnet": "9a0766d93b6608b7", + "testnet-fork": "9a0766d93b6608b7" } }, "IBridgePermissions": { @@ -635,7 +690,8 @@ "emulator": "f8d6e0586b0a20c7", "mainnet": "1e4aa0b87d10b141", "mainnet-fork": "1e4aa0b87d10b141", - "testnet": "dfc20aee650fcbdf" + "testnet": "dfc20aee650fcbdf", + "testnet-fork": "dfc20aee650fcbdf" } }, "ICrossVM": { @@ -647,7 +703,8 @@ "mainnet": "1e4aa0b87d10b141", "mainnet-fork": "1e4aa0b87d10b141", "testing": "0000000000000001", - "testnet": "dfc20aee650fcbdf" + "testnet": "dfc20aee650fcbdf", + "testnet-fork": "dfc20aee650fcbdf" } }, "ICrossVMAsset": { @@ -659,7 +716,8 @@ "mainnet": "1e4aa0b87d10b141", "mainnet-fork": "1e4aa0b87d10b141", "testing": "0000000000000001", - "testnet": "dfc20aee650fcbdf" + "testnet": "dfc20aee650fcbdf", + "testnet-fork": "dfc20aee650fcbdf" } }, "IEVMBridgeNFTMinter": { @@ -671,7 +729,8 @@ "mainnet": "1e4aa0b87d10b141", "mainnet-fork": "1e4aa0b87d10b141", "testing": "0000000000000001", - "testnet": "dfc20aee650fcbdf" + "testnet": "dfc20aee650fcbdf", + "testnet-fork": "dfc20aee650fcbdf" } }, "IEVMBridgeTokenMinter": { @@ -683,7 +742,8 @@ "mainnet": "1e4aa0b87d10b141", "mainnet-fork": "1e4aa0b87d10b141", "testing": "0000000000000001", - "testnet": "dfc20aee650fcbdf" + "testnet": "dfc20aee650fcbdf", + "testnet-fork": "dfc20aee650fcbdf" } }, "IFlowEVMNFTBridge": { @@ -695,7 +755,8 @@ "mainnet": "1e4aa0b87d10b141", "mainnet-fork": "1e4aa0b87d10b141", "testing": "0000000000000001", - "testnet": "dfc20aee650fcbdf" + "testnet": "dfc20aee650fcbdf", + "testnet-fork": "dfc20aee650fcbdf" } }, "IFlowEVMTokenBridge": { @@ -707,7 +768,8 @@ "mainnet": "1e4aa0b87d10b141", "mainnet-fork": "1e4aa0b87d10b141", "testing": "0000000000000001", - "testnet": "dfc20aee650fcbdf" + "testnet": "dfc20aee650fcbdf", + "testnet-fork": "dfc20aee650fcbdf" } }, "MetadataViews": { @@ -718,7 +780,8 @@ "emulator": "f8d6e0586b0a20c7", "mainnet": "1d7e57aa55817448", "mainnet-fork": "1d7e57aa55817448", - "testnet": "631e88ae7f1d7c20" + "testnet": "631e88ae7f1d7c20", + "testnet-fork": "631e88ae7f1d7c20" } }, "NonFungibleToken": { @@ -729,7 +792,8 @@ "emulator": "f8d6e0586b0a20c7", "mainnet": "1d7e57aa55817448", "mainnet-fork": "1d7e57aa55817448", - "testnet": "631e88ae7f1d7c20" + "testnet": "631e88ae7f1d7c20", + "testnet-fork": "631e88ae7f1d7c20" } }, "ScopedFTProviders": { @@ -741,7 +805,8 @@ "mainnet": "1e4aa0b87d10b141", "mainnet-fork": "1e4aa0b87d10b141", "testing": "0000000000000001", - "testnet": "dfc20aee650fcbdf" + "testnet": "dfc20aee650fcbdf", + "testnet-fork": "dfc20aee650fcbdf" } }, "Serialize": { @@ -862,7 +927,8 @@ "emulator": "f8d6e0586b0a20c7", "mainnet": "1d7e57aa55817448", "mainnet-fork": "1d7e57aa55817448", - "testnet": "631e88ae7f1d7c20" + "testnet": "631e88ae7f1d7c20", + "testnet-fork": "631e88ae7f1d7c20" } } }, @@ -874,7 +940,11 @@ "fork": "mainnet" }, "testing": "127.0.0.1:3569", - "testnet": "access.devnet.nodes.onflow.org:9000" + "testnet": "access.devnet.nodes.onflow.org:9000", + "testnet-fork": { + "host": "127.0.0.1:3569", + "fork": "testnet" + } }, "accounts": { "emulator-account": { @@ -958,6 +1028,14 @@ "resourceID": "projects/dl-flow-devex-staging/locations/us-central1/keyRings/tidal-keyring/cryptoKeys/tidal_admin_pk/cryptoKeyVersions/1" } }, + "testnet-fork-admin": { + "address": "d2580caf2ef07c2f", + "key": { + "type": "file", + "location": "local/emulator-account.pkey" + } + + }, "testnet-flow-alp-deployer": { "address": "426f0458ced60037", "key": { @@ -1207,7 +1285,68 @@ "FlowYieldVaultsSchedulerV1", "FlowYieldVaultsClosedBeta", "FlowYieldVaults", - "MockStrategies", + { + "name": "FlowYieldVaultsStrategiesV2", + "args": [ + { + "value": "0x92657b195e22b69E4779BBD09Fa3CD46F0CF8e39", + "type": "String" + }, + { + "value": "0x2Db6468229F6fB1a77d248Dbb1c386760C257804", + "type": "String" + }, + { + "value": "0xA1e0E4CCACA34a738f03cFB1EAbAb16331FA3E2c", + "type": "String" + } + ] + }, + { + "name": "PMStrategiesV1", + "args": [ + { + "value": "0x92657b195e22b69E4779BBD09Fa3CD46F0CF8e39", + "type": "String" + }, + { + "value": "0x2Db6468229F6fB1a77d248Dbb1c386760C257804", + "type": "String" + }, + { + "value": "0xA1e0E4CCACA34a738f03cFB1EAbAb16331FA3E2c", + "type": "String" + } + ] + } + ] + }, + "testnet-fork": { + "testnet-fork-admin": [ + { + "name": "YieldToken", + "args": [ + { + "value": "1000000.00000000", + "type": "UFix64" + } + ] + }, + { + "name": "MockOracle", + "args": [ + { + "value": "A.426f0458ced60037.MOET.Vault", + "type": "String" + } + ] + }, + "MockSwapper", + "FlowYieldVaultsSchedulerRegistry", + "FlowYieldVaultsAutoBalancers", + "FlowYieldVaultsSchedulerV1", + "FlowYieldVaultsClosedBeta", + "FlowYieldVaults", { "name": "FlowYieldVaultsStrategiesV2", "args": [ diff --git a/local/setup_testnet.sh b/local/setup_testnet.sh index e1fb2697..40f47131 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 @@ -90,13 +105,13 @@ flow transactions send ./cadence/transactions/flow-yield-vaults/admin/add_strate ## PYUSD0 Vault # WFLOW univ3 path and fees -# path: FUSDEV - WFLOW +# path: FUSDEV - MOET - 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", "0xf622664Ba813e63947Cfa6c2E95E5c18F617E6C9", "0xd3bF53DAC106A0290B0483EcBC89d40FcC961f3e"]' \ + '[100, 3000]' \ --network testnet \ --signer testnet-admin @@ -106,22 +121,46 @@ flow transactions send ./cadence/transactions/flow-yield-vaults/admin/upsert_str 'A.d2580caf2ef07c2f.FlowYieldVaultsStrategiesV2.FUSDEVStrategy' \ 'A.dfc20aee650fcbdf.EVMVMBridgedToken_059a77239dafa770977dd9f1e98632c3e4559848.Vault' \ "0x61b44D19486EE492449E83C1201581C754e9e1E1" \ - '["0x61b44D19486EE492449E83C1201581C754e9e1E1","0x02d3575e2516a515E9B91a52b294Edc80DC7987c", "0x059A77239daFa770977DD9f1E98632C3E4559848"]' \ - '[3000,3000]' \ + '["0x61b44D19486EE492449E83C1201581C754e9e1E1","0xf622664Ba813e63947Cfa6c2E95E5c18F617E6C9", "0x059A77239daFa770977DD9f1E98632C3E4559848"]' \ + '[100,3000]' \ --network testnet \ --signer testnet-admin # WBTC univ3 path and fees -# path: FUSDEV - MOET - WETH +# path: FUSDEV - MOET - 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","0xf622664Ba813e63947Cfa6c2E95E5c18F617E6C9","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 + +# configure PYUSD0 as MOET-preswap collateral for FUSDEVStrategy (MorphoERC4626StrategyComposer) +# path: PYUSD0 → MOET (1-hop, fee 100 = 0.01%) +# testnet PYUSD0 EVM: 0xd7d43ab7b365f0d0789ae83f4385fa710ffdc98f +# testnet MOET EVM: 0xf622664Ba813e63947Cfa6c2E95E5c18F617E6C9 +# flow transactions send ./cadence/transactions/flow-yield-vaults/admin/upsert_moet_preswap_config.cdc \ +# 'A.d2580caf2ef07c2f.FlowYieldVaultsStrategiesV2.MorphoERC4626StrategyComposer' \ +# 'A.dfc20aee650fcbdf.EVMVMBridgedToken_d7d43ab7b365f0d0789ae83f4385fa710ffdc98f.Vault' \ +# '["0xd7d43ab7b365f0d0789ae83f4385fa710ffdc98f","0xf622664Ba813e63947Cfa6c2E95E5c18F617E6C9"]' \ +# '[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 +168,79 @@ 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 +# MOET: 0xf622664Ba813e63947Cfa6c2E95E5c18F617E6C9 (bridged; used as intermediate) +# 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 MOET as the intermediate hop (mirrors testnet FUSDEVStrategy pool structure) + +# WBTC collateral — syWFLOWv → WFLOW (fee 100), WFLOW → MOET → 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","0xf622664Ba813e63947Cfa6c2E95E5c18F617E6C9","0x208d09d2a6Dd176e3e95b3F0DE172A7471C5B2d6"]' \ + '[3000,3000]' \ + --network testnet \ + --signer testnet-admin + +# WETH collateral — syWFLOWv → WFLOW (fee 100), WFLOW → MOET → 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","0xf622664Ba813e63947Cfa6c2E95E5c18F617E6C9","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 + +# configure PYUSD0 as MOET-preswap collateral for syWFLOWvStrategy (MoreERC4626StrategyComposer) +# path: PYUSD0 → MOET (1-hop, fee 100 = 0.01%) +# testnet PYUSD0 EVM: 0xd7d43ab7b365f0d0789ae83f4385fa710ffdc98f +# testnet MOET EVM: 0xf622664Ba813e63947Cfa6c2E95E5c18F617E6C9 +flow transactions send ./cadence/transactions/flow-yield-vaults/admin/upsert_moet_preswap_config.cdc \ + 'A.d2580caf2ef07c2f.FlowYieldVaultsStrategiesV2.MoreERC4626StrategyComposer' \ + 'A.dfc20aee650fcbdf.EVMVMBridgedToken_d7d43ab7b365f0d0789ae83f4385fa710ffdc98f.Vault' \ + '["0xd7d43ab7b365f0d0789ae83f4385fa710ffdc98f","0xf622664Ba813e63947Cfa6c2E95E5c18F617E6C9"]' \ + '[100]' \ + --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' \ diff --git a/local/setup_testnet_fork.sh b/local/setup_testnet_fork.sh new file mode 100755 index 00000000..9c204a64 --- /dev/null +++ b/local/setup_testnet_fork.sh @@ -0,0 +1,121 @@ +#!/usr/bin/env bash +# Setup script for the testnet-fork emulator. +# +# Start the emulator first with: +# flow emulator --fork testnet +# +# Then run this script to redeploy local contracts and reconfigure state. + +set -e + +# install dependencies (skip alias prompts and deployments — we handle those below) +flow deps install --skip-alias --skip-deployments + +# Redeploy updated local contracts over the forked testnet state. +# All other contracts (FungibleToken, EVM, FlowALPv0, etc.) are already live +# on testnet and accessible in the fork without redeployment. +flow project deploy --network testnet-fork --update + +# Remove the stale FlowYieldVaultsStrategies.TracerStrategy from the StrategyFactory. +# +# The old FlowYieldVaultsStrategies contract on testnet has TracerStrategy that no longer +# conforms to FlowYieldVaults.Strategy (missing closePosition). This blocks deserialization +# of the entire StrategyFactory, causing createYieldVault to fail for ALL strategies. +# +# The FlowYieldVaultsStrategies stub deployed above fixes the type-check so the factory can +# be deserialized; this call then permanently removes the stale entry. +flow transactions send ./cadence/transactions/flow-yield-vaults/admin/remove_strategy_composer.cdc \ + 'A.d2580caf2ef07c2f.FlowYieldVaultsStrategies.TracerStrategy' \ + --network testnet-fork --signer testnet-fork-admin + +# Also remove MockStrategies.TracerStrategy if present (registered during testnet setup; +# not needed for production debugging of create_yield_vault). +flow transactions send ./cadence/transactions/flow-yield-vaults/admin/remove_strategy_composer.cdc \ + 'A.d2580caf2ef07c2f.MockStrategies.TracerStrategy' \ + --network testnet-fork --signer testnet-fork-admin + +# Set mock oracle prices (FLOW = $0.5, YieldToken = $1.0) +flow transactions send ./cadence/transactions/mocks/oracle/set_price.cdc \ + 'A.7e60df042a9c0868.FlowToken.Vault' 0.5 \ + --network testnet-fork --signer testnet-fork-admin + +flow transactions send ./cadence/transactions/mocks/oracle/set_price.cdc \ + 'A.d2580caf2ef07c2f.YieldToken.Vault' 1.0 \ + --network testnet-fork --signer testnet-fork-admin + +# Wire up MockSwapper liquidity connectors +flow transactions send ./lib/FlowALP/cadence/transactions/moet/setup_vault.cdc \ + --network testnet-fork --signer testnet-fork-admin +flow transactions send ./cadence/transactions/mocks/swapper/set_liquidity_connector.cdc \ + /storage/flowTokenVault \ + --network testnet-fork --signer testnet-fork-admin +flow transactions send ./cadence/transactions/mocks/swapper/set_liquidity_connector.cdc \ + /storage/moetTokenVault_0x426f0458ced60037 \ + --network testnet-fork --signer testnet-fork-admin + +# Re-register FUSDEVStrategy composer (testnet address: d2580caf2ef07c2f) +flow transactions send ./cadence/transactions/flow-yield-vaults/admin/add_strategy_composer.cdc \ + 'A.d2580caf2ef07c2f.FlowYieldVaultsStrategiesV2.FUSDEVStrategy' \ + 'A.d2580caf2ef07c2f.FlowYieldVaultsStrategiesV2.MorphoERC4626StrategyComposer' \ + /storage/FlowYieldVaultsStrategyV2ComposerIssuer_0xd2580caf2ef07c2f \ + --network testnet-fork --signer testnet-fork-admin + +# Configure FUSDEVStrategy collateral paths. +# +# The testnet state may have a stale 2-element path [FUSDEV, WFLOW] for FlowToken.Vault +# collateral, but the contract now requires yieldToCollateral path length >= 3. +# Use [FUSDEV, MOET, WFLOW] fees [100, 3000]: +# - FUSDEV/MOET fee100 pool exists on testnet +# - MOET/WFLOW fee3000 pool exists on testnet +# - _createCollateralToDebtSwapper uses the last fee (3000) for WFLOW→PYUSD0, +# and the WFLOW/PYUSD0 fee3000 pool exists on testnet. +# +# Testnet EVM addresses: +# FUSDEV: 0x61b44D19486EE492449E83C1201581C754e9e1E1 +# MOET: 0xf622664Ba813e63947Cfa6c2E95E5c18F617E6C9 +# WFLOW: 0xd3bF53DAC106A0290B0483EcBC89d40FcC961f3e +# WETH: 0x059A77239daFa770977DD9f1E98632C3E4559848 +# WBTC: 0x208d09d2a6Dd176e3e95b3F0DE172A7471C5B2d6 + +# FlowToken.Vault (WFLOW) collateral — path: FUSDEV → MOET → WFLOW, fees [100, 3000] +flow transactions send ./cadence/transactions/flow-yield-vaults/admin/upsert_strategy_config.cdc \ + 'A.d2580caf2ef07c2f.FlowYieldVaultsStrategiesV2.FUSDEVStrategy' \ + 'A.7e60df042a9c0868.FlowToken.Vault' \ + '0x61b44D19486EE492449E83C1201581C754e9e1E1' \ + '["0x61b44D19486EE492449E83C1201581C754e9e1E1","0xf622664Ba813e63947Cfa6c2E95E5c18F617E6C9","0xd3bF53DAC106A0290B0483EcBC89d40FcC961f3e"]' \ + '[100,3000]' \ + --network testnet-fork --signer testnet-fork-admin + +# WETH collateral — path: FUSDEV → MOET → WETH, fees [100, 3000] +flow transactions send ./cadence/transactions/flow-yield-vaults/admin/upsert_strategy_config.cdc \ + 'A.d2580caf2ef07c2f.FlowYieldVaultsStrategiesV2.FUSDEVStrategy' \ + 'A.dfc20aee650fcbdf.EVMVMBridgedToken_059a77239dafa770977dd9f1e98632c3e4559848.Vault' \ + '0x61b44D19486EE492449E83C1201581C754e9e1E1' \ + '["0x61b44D19486EE492449E83C1201581C754e9e1E1","0xf622664Ba813e63947Cfa6c2E95E5c18F617E6C9","0x059A77239daFa770977DD9f1E98632C3E4559848"]' \ + '[100,3000]' \ + --network testnet-fork --signer testnet-fork-admin + +# WBTC collateral — path: FUSDEV → MOET → WBTC, fees [100, 3000] +flow transactions send ./cadence/transactions/flow-yield-vaults/admin/upsert_strategy_config.cdc \ + 'A.d2580caf2ef07c2f.FlowYieldVaultsStrategiesV2.FUSDEVStrategy' \ + 'A.dfc20aee650fcbdf.EVMVMBridgedToken_208d09d2a6dd176e3e95b3f0de172a7471c5b2d6.Vault' \ + '0x61b44D19486EE492449E83C1201581C754e9e1E1' \ + '["0x61b44D19486EE492449E83C1201581C754e9e1E1","0xf622664Ba813e63947Cfa6c2E95E5c18F617E6C9","0x208d09d2a6Dd176e3e95b3F0DE172A7471C5B2d6"]' \ + '[100,3000]' \ + --network testnet-fork --signer testnet-fork-admin + +# Grant beta access to a test user: +# flow transactions send ./cadence/transactions/flow-yield-vaults/admin/grant_beta.cdc \ +# --authorizer testnet-fork-admin, \ +# --proposer testnet-fork-admin \ +# --payer testnet-fork-admin \ +# --network testnet-fork + +# Send the create_yield_vault transaction for debugging: +# flow transactions send ./cadence/transactions/flow-yield-vaults/create_yield_vault.cdc \ +# 'A.d2580caf2ef07c2f.FlowYieldVaultsStrategiesV2.FUSDEVStrategy' \ +# 'A.7e60df042a9c0868.FlowToken.Vault' \ +# 1.0 \ +# --compute-limit 9999 \ +# --network testnet-fork \ +# --signer From 6e4e5788da5fd66b5d6b49a33df97d657fe4133b Mon Sep 17 00:00:00 2001 From: Alex <12097569+nialexsan@users.noreply.github.com> Date: Wed, 11 Mar 2026 09:47:37 -0400 Subject: [PATCH 21/27] add min amounts for testnet --- local/setup_mainnet.sh | 54 ++++++++++++++++++++++++++++++++++++++++++ local/setup_testnet.sh | 14 +++++++++++ 2 files changed, 68 insertions(+) diff --git a/local/setup_mainnet.sh b/local/setup_mainnet.sh index a092a621..da45c1ca 100755 --- a/local/setup_mainnet.sh +++ b/local/setup_mainnet.sh @@ -197,6 +197,60 @@ flow transactions send ./cadence/transactions/flow-yield-vaults/admin/add_strate --network mainnet \ --signer mainnet-admin +# configure syWFLOWvStrategy (MoreERC4626) collateral configs +# +# PYUSD0: yieldToUnderlying = syWFLOWv→WFLOW (fee 100), debtToCollateral = WFLOW→PYUSD0 (fee 500) +flow transactions send ./cadence/transactions/flow-yield-vaults/admin/upsert_more_erc4626_config.cdc \ + 'A.b1d63873c3cc9f79.FlowYieldVaultsStrategiesV2.syWFLOWvStrategy' \ + 'A.1e4aa0b87d10b141.EVMVMBridgedToken_99af3eea856556646c98c8b9b2548fe815240750.Vault' \ + '0xCBf9a7753F9D2d0e8141ebB36d99f87AcEf98597' \ + '["0xCBf9a7753F9D2d0e8141ebB36d99f87AcEf98597","0xd3bF53DAC106A0290B0483EcBC89d40FcC961f3e"]' \ + '[100]' \ + '["0xd3bF53DAC106A0290B0483EcBC89d40FcC961f3e","0x99aF3EeA856556646C98c8B9b2548Fe815240750"]' \ + '[500]' \ + --network mainnet \ + --signer mainnet-admin + +# MOET pre-swap: PYUSD0→MOET via UniV3 fee 100 (FlowALP only accepts MOET as stablecoin collateral) +flow transactions send ./cadence/transactions/flow-yield-vaults/admin/upsert_moet_preswap_config.cdc \ + 'A.b1d63873c3cc9f79.FlowYieldVaultsStrategiesV2.MoreERC4626StrategyComposer' \ + 'A.1e4aa0b87d10b141.EVMVMBridgedToken_99af3eea856556646c98c8b9b2548fe815240750.Vault' \ + '["0x99aF3EeA856556646C98c8B9b2548Fe815240750","0x213979bb8a9a86966999b3aa797c1fcf3b967ae2"]' \ + '[100]' \ + --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.syWFLOWvStrategy' \ diff --git a/local/setup_testnet.sh b/local/setup_testnet.sh index 40f47131..4f0ded05 100755 --- a/local/setup_testnet.sh +++ b/local/setup_testnet.sh @@ -62,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' \ @@ -72,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 From dc1ee72ae87297ce08cee6ae0d988760e47e56e9 Mon Sep 17 00:00:00 2001 From: Alex <12097569+nialexsan@users.noreply.github.com> Date: Wed, 11 Mar 2026 12:59:51 -0400 Subject: [PATCH 22/27] fix mainnet setup scripts --- .../FlowYieldVaultsStrategiesV2_syWFLOWv_test.cdc | 4 ++-- flow.json | 10 ---------- local/setup_mainnet.sh | 15 +++------------ 3 files changed, 5 insertions(+), 24 deletions(-) diff --git a/cadence/tests/FlowYieldVaultsStrategiesV2_syWFLOWv_test.cdc b/cadence/tests/FlowYieldVaultsStrategiesV2_syWFLOWv_test.cdc index 68756367..c010c9ed 100644 --- a/cadence/tests/FlowYieldVaultsStrategiesV2_syWFLOWv_test.cdc +++ b/cadence/tests/FlowYieldVaultsStrategiesV2_syWFLOWv_test.cdc @@ -219,7 +219,7 @@ access(all) fun setup() { // 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 500)...") + log("Configuring MoreERC4626CollateralConfig: syWFLOWvStrategy + PYUSD0 (WFLOW→PYUSD0 fee 3000)...") result = _executeTransactionFile( "../transactions/flow-yield-vaults/admin/upsert_more_erc4626_config.cdc", [ @@ -229,7 +229,7 @@ access(all) fun setup() { [syWFLOWvEVMAddress, wflowEVMAddress], // yieldToUnderlying [100 as UInt32], [wflowEVMAddress, pyusd0EVMAddress], // debtToCollateral - [500 as UInt32] + [3000 as UInt32] ], [adminAccount] ) diff --git a/flow.json b/flow.json index 8c351dbc..02e86bc9 100644 --- a/flow.json +++ b/flow.json @@ -1154,16 +1154,6 @@ }, "mainnet": { "mainnet-admin": [ - { - "name": "MockOracle", - "args": [ - { - "value": "A.6b00ff876c299c61.MOET.Vault", - "type": "String" - } - ] - }, - "MockSwapper", "FlowYieldVaultsSchedulerRegistry", "FlowYieldVaultsAutoBalancers", "FlowYieldVaultsSchedulerV1", diff --git a/local/setup_mainnet.sh b/local/setup_mainnet.sh index da45c1ca..4e9dd68e 100755 --- a/local/setup_mainnet.sh +++ b/local/setup_mainnet.sh @@ -179,16 +179,6 @@ flow transactions send ./cadence/transactions/flow-yield-vaults/admin/upsert_str '[100,3000]' \ --network mainnet \ --signer mainnet-admin -# -# Setup UniV3 path FUSDEV -> PYUSD0 -flow transactions send ./cadence/transactions/flow-yield-vaults/admin/upsert_strategy_config.cdc \ - 'A.b1d63873c3cc9f79.FlowYieldVaultsStrategiesV2.FUSDEVStrategy' \ - 'A.1e4aa0b87d10b141.EVMVMBridgedToken_99af3eea856556646c98c8b9b2548fe815240750.Vault' \ - "0xd069d989e2F44B70c65347d1853C0c67e10a9F8D" \ - '["0xd069d989e2F44B70c65347d1853C0c67e10a9F8D","0x99aF3EeA856556646C98c8B9b2548Fe815240750"]' \ - '[100]' \ - --network mainnet \ - --signer mainnet-admin flow transactions send ./cadence/transactions/flow-yield-vaults/admin/add_strategy_composer.cdc \ 'A.b1d63873c3cc9f79.FlowYieldVaultsStrategiesV2.FUSDEVStrategy' \ @@ -199,7 +189,8 @@ flow transactions send ./cadence/transactions/flow-yield-vaults/admin/add_strate # configure syWFLOWvStrategy (MoreERC4626) collateral configs # -# PYUSD0: yieldToUnderlying = syWFLOWv→WFLOW (fee 100), debtToCollateral = WFLOW→PYUSD0 (fee 500) +# PYUSD0: yieldToUnderlying = syWFLOWv→WFLOW (fee 100), debtToCollateral = WFLOW→PYUSD0 (fee 3000) +# Note: no WFLOW/PYUSD0 fee500 pool exists on mainnet — use fee3000 (pool 0x0fdba612...). flow transactions send ./cadence/transactions/flow-yield-vaults/admin/upsert_more_erc4626_config.cdc \ 'A.b1d63873c3cc9f79.FlowYieldVaultsStrategiesV2.syWFLOWvStrategy' \ 'A.1e4aa0b87d10b141.EVMVMBridgedToken_99af3eea856556646c98c8b9b2548fe815240750.Vault' \ @@ -207,7 +198,7 @@ flow transactions send ./cadence/transactions/flow-yield-vaults/admin/upsert_mor '["0xCBf9a7753F9D2d0e8141ebB36d99f87AcEf98597","0xd3bF53DAC106A0290B0483EcBC89d40FcC961f3e"]' \ '[100]' \ '["0xd3bF53DAC106A0290B0483EcBC89d40FcC961f3e","0x99aF3EeA856556646C98c8B9b2548Fe815240750"]' \ - '[500]' \ + '[3000]' \ --network mainnet \ --signer mainnet-admin From 0cbb3312b2a1ae2cedf8a23db67e9af99f26986e Mon Sep 17 00:00:00 2001 From: Alex <12097569+nialexsan@users.noreply.github.com> Date: Wed, 11 Mar 2026 16:11:54 -0400 Subject: [PATCH 23/27] remove debug scripts --- ...ssert_flowalp_position_moet_collateral.cdc | 41 ------------------- .../get_flowalp_position_ids.cdc | 10 ----- 2 files changed, 51 deletions(-) delete mode 100644 cadence/scripts/flow-yield-vaults/assert_flowalp_position_moet_collateral.cdc delete mode 100644 cadence/scripts/flow-yield-vaults/get_flowalp_position_ids.cdc diff --git a/cadence/scripts/flow-yield-vaults/assert_flowalp_position_moet_collateral.cdc b/cadence/scripts/flow-yield-vaults/assert_flowalp_position_moet_collateral.cdc deleted file mode 100644 index ba6693f4..00000000 --- a/cadence/scripts/flow-yield-vaults/assert_flowalp_position_moet_collateral.cdc +++ /dev/null @@ -1,41 +0,0 @@ -import "FlowALPv0" - -/// Verifies the PYUSD0→MOET pre-swap invariant for the FlowALP pool: -/// -/// 1. PYUSD0 is NOT a supported collateral token in FlowALP (so it cannot be deposited directly). -/// 2. MOET (the pool's default token) IS a supported collateral token. -/// -/// Together with a successful PYUSD0 vault creation test, this proves that the strategy -/// pre-swapped PYUSD0 → MOET before depositing into FlowALP — since FlowALP cannot receive -/// PYUSD0 directly. -/// -/// Returns a string starting with "OK:" on success or "FAIL:" on failure. -access(all) fun main(): String { - let pool = getAccount(0x6b00ff876c299c61) - .capabilities.borrow<&FlowALPv0.Pool>(FlowALPv0.PoolPublicPath) - ?? panic("Could not borrow FlowALP pool") - - let pyusd0TypeID = "A.1e4aa0b87d10b141.EVMVMBridgedToken_99af3eea856556646c98c8b9b2548fe815240750.Vault" - let supportedTokens = pool.getSupportedTokens() - - // Assert PYUSD0 is NOT supported (FlowALP cannot receive it as collateral) - for t in supportedTokens { - if t.identifier == pyusd0TypeID { - return "FAIL: PYUSD0 is listed as a supported FlowALP token — pre-swap may not be required" - } - } - - // Assert MOET (default token) IS supported - let defaultToken = pool.getDefaultToken() - var moetSupported = false - for t in supportedTokens { - if t == defaultToken { - moetSupported = true - } - } - if !moetSupported { - return "FAIL: MOET (pool default token) is not in the supported tokens list" - } - - return "OK: PYUSD0 is not a supported FlowALP collateral; MOET (".concat(defaultToken.identifier).concat(") is — pre-swap invariant confirmed") -} diff --git a/cadence/scripts/flow-yield-vaults/get_flowalp_position_ids.cdc b/cadence/scripts/flow-yield-vaults/get_flowalp_position_ids.cdc deleted file mode 100644 index 4fce8050..00000000 --- a/cadence/scripts/flow-yield-vaults/get_flowalp_position_ids.cdc +++ /dev/null @@ -1,10 +0,0 @@ -import "FlowALPv0" - -/// Returns all position IDs currently in the FlowALP pool at the hardcoded mainnet address. -/// Used in fork tests to snapshot existing positions before a test creates new ones. -access(all) fun main(): [UInt64] { - let pool = getAccount(0x6b00ff876c299c61) - .capabilities.borrow<&FlowALPv0.Pool>(FlowALPv0.PoolPublicPath) - ?? panic("Could not borrow FlowALP pool") - return pool.getPositionIDs() -} From 7b776741b323b711570f33f46128ff0f2f7b4573 Mon Sep 17 00:00:00 2001 From: Alex <12097569+nialexsan@users.noreply.github.com> Date: Thu, 12 Mar 2026 12:15:34 -0400 Subject: [PATCH 24/27] mainnet testing scripts --- local/setup_mainnet.sh | 42 +++++++++++++++++++++++++++++++++++++++++- 1 file changed, 41 insertions(+), 1 deletion(-) diff --git a/local/setup_mainnet.sh b/local/setup_mainnet.sh index 4e9dd68e..81bdb8fa 100755 --- a/local/setup_mainnet.sh +++ b/local/setup_mainnet.sh @@ -296,6 +296,8 @@ flow transactions send ./lib/FlowALP/cadence/tests/transactions/flow-alp/pool-ma # test FlowYieldVault strategy # +# FUSDEV Strategy +# # WFLOW (FLOW) # flow transactions send ./cadence/transactions/flow-yield-vaults/create_yield_vault.cdc \ # A.b1d63873c3cc9f79.FlowYieldVaultsStrategiesV2.FUSDEVStrategy \ @@ -323,7 +325,7 @@ flow transactions send ./lib/FlowALP/cadence/tests/transactions/flow-alp/pool-ma # --network mainnet \ # --signer # -# PYUSD0 +# PYUSD0 - should fail # flow transactions send ./cadence/transactions/flow-yield-vaults/create_yield_vault.cdc \ # A.b1d63873c3cc9f79.FlowYieldVaultsStrategiesV2.FUSDEVStrategy \ # A.1e4aa0b87d10b141.EVMVMBridgedToken_99af3eea856556646c98c8b9b2548fe815240750.Vault \ @@ -332,6 +334,44 @@ flow transactions send ./lib/FlowALP/cadence/tests/transactions/flow-alp/pool-ma # --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 # # WFLOW From 45830c9385c85a86132742a3fe01f9244b57d6f1 Mon Sep 17 00:00:00 2001 From: Alex <12097569+nialexsan@users.noreply.github.com> Date: Fri, 13 Mar 2026 13:49:37 -0400 Subject: [PATCH 25/27] fixed access --- cadence/contracts/FlowYieldVaultsStrategiesV2.cdc | 2 +- local/setup_mainnet.sh | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/cadence/contracts/FlowYieldVaultsStrategiesV2.cdc b/cadence/contracts/FlowYieldVaultsStrategiesV2.cdc index 060b4301..a7d244a2 100644 --- a/cadence/contracts/FlowYieldVaultsStrategiesV2.cdc +++ b/cadence/contracts/FlowYieldVaultsStrategiesV2.cdc @@ -1997,7 +1997,7 @@ access(all) contract FlowYieldVaultsStrategiesV2 { /// Creates a fresh StrategyComposerIssuer with the default config skeleton. /// Intended for the deployer account to recreate a lost or destroyed issuer via a transaction. - access(all) + access(account) fun createIssuer(): @StrategyComposerIssuer { return <- create StrategyComposerIssuer( configs: { diff --git a/local/setup_mainnet.sh b/local/setup_mainnet.sh index 81bdb8fa..c5a55f4a 100755 --- a/local/setup_mainnet.sh +++ b/local/setup_mainnet.sh @@ -190,7 +190,6 @@ flow transactions send ./cadence/transactions/flow-yield-vaults/admin/add_strate # configure syWFLOWvStrategy (MoreERC4626) collateral configs # # PYUSD0: yieldToUnderlying = syWFLOWv→WFLOW (fee 100), debtToCollateral = WFLOW→PYUSD0 (fee 3000) -# Note: no WFLOW/PYUSD0 fee500 pool exists on mainnet — use fee3000 (pool 0x0fdba612...). flow transactions send ./cadence/transactions/flow-yield-vaults/admin/upsert_more_erc4626_config.cdc \ 'A.b1d63873c3cc9f79.FlowYieldVaultsStrategiesV2.syWFLOWvStrategy' \ 'A.1e4aa0b87d10b141.EVMVMBridgedToken_99af3eea856556646c98c8b9b2548fe815240750.Vault' \ From 0dceb7515d589e79e14ccf4a78fab0b5acb6944f Mon Sep 17 00:00:00 2001 From: Alex <12097569+nialexsan@users.noreply.github.com> Date: Fri, 13 Mar 2026 15:48:37 -0400 Subject: [PATCH 26/27] remove compose issuer public method --- cadence/contracts/FlowYieldVaultsStrategiesV2.cdc | 13 ------------- 1 file changed, 13 deletions(-) diff --git a/cadence/contracts/FlowYieldVaultsStrategiesV2.cdc b/cadence/contracts/FlowYieldVaultsStrategiesV2.cdc index a7d244a2..22e19374 100644 --- a/cadence/contracts/FlowYieldVaultsStrategiesV2.cdc +++ b/cadence/contracts/FlowYieldVaultsStrategiesV2.cdc @@ -1995,19 +1995,6 @@ access(all) contract FlowYieldVaultsStrategiesV2 { } } - /// Creates a fresh StrategyComposerIssuer with the default config skeleton. - /// Intended for the deployer account to recreate a lost or destroyed issuer via a transaction. - access(account) - fun createIssuer(): @StrategyComposerIssuer { - return <- create StrategyComposerIssuer( - configs: { - Type<@MorphoERC4626StrategyComposer>(): { - Type<@FUSDEVStrategy>(): {} as {Type: CollateralConfig} - } - } - ) - } - /// Returns the COA capability for this account /// TODO: this is temporary until we have a better way to pass user's COAs to inner connectors access(self) From 0480061c070ddcd8c961ba9ff2e4d8eee9fcf1e0 Mon Sep 17 00:00:00 2001 From: Alex <12097569+nialexsan@users.noreply.github.com> Date: Fri, 13 Mar 2026 15:53:30 -0400 Subject: [PATCH 27/27] remove unnecessary recreate issuer --- ...lowYieldVaultsStrategiesV2_FUSDEV_test.cdc | 9 -------- ...wYieldVaultsStrategiesV2_syWFLOWv_test.cdc | 9 -------- .../admin/recreate_composer_issuer.cdc | 23 ------------------- 3 files changed, 41 deletions(-) delete mode 100644 cadence/transactions/flow-yield-vaults/admin/recreate_composer_issuer.cdc diff --git a/cadence/tests/FlowYieldVaultsStrategiesV2_FUSDEV_test.cdc b/cadence/tests/FlowYieldVaultsStrategiesV2_FUSDEV_test.cdc index be4f11d3..34c2c202 100644 --- a/cadence/tests/FlowYieldVaultsStrategiesV2_FUSDEV_test.cdc +++ b/cadence/tests/FlowYieldVaultsStrategiesV2_FUSDEV_test.cdc @@ -213,15 +213,6 @@ access(all) fun setup() { ) Test.expect(err, Test.beNil()) - // Recreate the StrategyComposerIssuer (deleted from mainnet storage on contract redeploy). - log("Recreating StrategyComposerIssuer...") - var result = _executeTransactionFile( - "../transactions/flow-yield-vaults/admin/recreate_composer_issuer.cdc", - [], - [adminAccount] - ) - Test.expect(result, Test.beSucceeded()) - // Configure UniV3 paths for FUSDEVStrategy. // Closing direction: FUSDEV → PYUSD0 (Morpho redeem, fee 100) → (UniV3 swap, fee 3000). // PYUSD0 is intentionally NOT configured as collateral — it is the underlying asset. diff --git a/cadence/tests/FlowYieldVaultsStrategiesV2_syWFLOWv_test.cdc b/cadence/tests/FlowYieldVaultsStrategiesV2_syWFLOWv_test.cdc index c010c9ed..eee318c3 100644 --- a/cadence/tests/FlowYieldVaultsStrategiesV2_syWFLOWv_test.cdc +++ b/cadence/tests/FlowYieldVaultsStrategiesV2_syWFLOWv_test.cdc @@ -207,15 +207,6 @@ access(all) fun setup() { ) Test.expect(err, Test.beNil()) - // Recreate the StrategyComposerIssuer (deleted from mainnet storage). - log("Recreating StrategyComposerIssuer...") - var result = _executeTransactionFile( - "../transactions/flow-yield-vaults/admin/recreate_composer_issuer.cdc", - [], - [adminAccount] - ) - Test.expect(result, Test.beSucceeded()) - // yieldToUnderlying path is the same for all collaterals: syWFLOWv → WFLOW via UniV3 fee 100 (0.01%) // debtToCollateral paths differ per collateral: WFLOW → diff --git a/cadence/transactions/flow-yield-vaults/admin/recreate_composer_issuer.cdc b/cadence/transactions/flow-yield-vaults/admin/recreate_composer_issuer.cdc deleted file mode 100644 index 8ac3bbe5..00000000 --- a/cadence/transactions/flow-yield-vaults/admin/recreate_composer_issuer.cdc +++ /dev/null @@ -1,23 +0,0 @@ -import "FlowYieldVaultsStrategiesV2" - -/// Admin transaction to recreate the StrategyComposerIssuer resource at IssuerStoragePath. -/// -/// Use this if the issuer was accidentally destroyed or is missing from storage. -/// Initialises with the default config (MorphoERC4626StrategyComposer / FUSDEVStrategy skeleton) -/// — run upsert_strategy_config / upsert_more_erc4626_config afterwards to repopulate configs. -/// -/// Must be signed by the account that deployed FlowYieldVaultsStrategiesV2. -transaction { - prepare(acct: auth(Storage) &Account) { - // Destroy any existing issuer so we can replace it cleanly - if acct.storage.type(at: FlowYieldVaultsStrategiesV2.IssuerStoragePath) != nil { - let old <- acct.storage.load<@FlowYieldVaultsStrategiesV2.StrategyComposerIssuer>( - from: FlowYieldVaultsStrategiesV2.IssuerStoragePath - ) - destroy old - } - - let issuer <- FlowYieldVaultsStrategiesV2.createIssuer() - acct.storage.save(<-issuer, to: FlowYieldVaultsStrategiesV2.IssuerStoragePath) - } -}