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/72] 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/72] 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/72] 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/72] 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/72] 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/72] 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/72] 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/72] 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/72] 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/72] 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/72] 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/72] 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/72] 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/72] 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/72] 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/72] 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/72] 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/72] 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/72] 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/72] 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/72] 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/72] 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/72] 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/72] 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/72] 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/72] 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/72] 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) - } -} From 8983fb1e44c1a097d9d4e3c01377d538eddb9b41 Mon Sep 17 00:00:00 2001 From: Alex <12097569+nialexsan@users.noreply.github.com> Date: Mon, 16 Mar 2026 11:33:07 -0400 Subject: [PATCH 28/72] fix var --- cadence/tests/FlowYieldVaultsStrategiesV2_FUSDEV_test.cdc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cadence/tests/FlowYieldVaultsStrategiesV2_FUSDEV_test.cdc b/cadence/tests/FlowYieldVaultsStrategiesV2_FUSDEV_test.cdc index 34c2c202..e6d46eea 100644 --- a/cadence/tests/FlowYieldVaultsStrategiesV2_FUSDEV_test.cdc +++ b/cadence/tests/FlowYieldVaultsStrategiesV2_FUSDEV_test.cdc @@ -218,7 +218,7 @@ access(all) fun setup() { // PYUSD0 is intentionally NOT configured as collateral — it is the underlying asset. log("Configuring FUSDEVStrategy + WFLOW (FUSDEV→PYUSD0→WFLOW fees 100/3000)...") - result = _executeTransactionFile( + var result = _executeTransactionFile( "../transactions/flow-yield-vaults/admin/upsert_strategy_config.cdc", [ fusdEvStrategyIdentifier, From 83bd1c0bebb7f5c964fb990e6be735777f2f4d6f Mon Sep 17 00:00:00 2001 From: Alex <12097569+nialexsan@users.noreply.github.com> Date: Mon, 16 Mar 2026 11:38:40 -0400 Subject: [PATCH 29/72] fix var --- cadence/tests/FlowYieldVaultsStrategiesV2_syWFLOWv_test.cdc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cadence/tests/FlowYieldVaultsStrategiesV2_syWFLOWv_test.cdc b/cadence/tests/FlowYieldVaultsStrategiesV2_syWFLOWv_test.cdc index eee318c3..e4910ef3 100644 --- a/cadence/tests/FlowYieldVaultsStrategiesV2_syWFLOWv_test.cdc +++ b/cadence/tests/FlowYieldVaultsStrategiesV2_syWFLOWv_test.cdc @@ -211,7 +211,7 @@ access(all) fun setup() { // debtToCollateral paths differ per collateral: WFLOW → log("Configuring MoreERC4626CollateralConfig: syWFLOWvStrategy + PYUSD0 (WFLOW→PYUSD0 fee 3000)...") - result = _executeTransactionFile( + var result = _executeTransactionFile( "../transactions/flow-yield-vaults/admin/upsert_more_erc4626_config.cdc", [ syWFLOWvStrategyIdentifier, From 956665bf3f3b9b93daf74f3cab166f48437f5f69 Mon Sep 17 00:00:00 2001 From: Alex <12097569+nialexsan@users.noreply.github.com> Date: Mon, 16 Mar 2026 18:39:33 -0400 Subject: [PATCH 30/72] update ref --- lib/FlowALP | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/FlowALP b/lib/FlowALP index 7ceac067..3fc2ab24 160000 --- a/lib/FlowALP +++ b/lib/FlowALP @@ -1 +1 @@ -Subproject commit 7ceac06706e288b254a8ddf448540c7d3538a5d5 +Subproject commit 3fc2ab24ce0a8738a5bff1da781316c260663217 From 45dfd0502ca62c2a36f224495dbff5c002ebf0e7 Mon Sep 17 00:00:00 2001 From: Alex <12097569+nialexsan@users.noreply.github.com> Date: Tue, 17 Mar 2026 12:42:47 -0400 Subject: [PATCH 31/72] recreate swappers --- .../contracts/FlowYieldVaultsStrategiesV2.cdc | 278 +++++++++++++++--- 1 file changed, 229 insertions(+), 49 deletions(-) diff --git a/cadence/contracts/FlowYieldVaultsStrategiesV2.cdc b/cadence/contracts/FlowYieldVaultsStrategiesV2.cdc index 22e19374..00fd1a20 100644 --- a/cadence/contracts/FlowYieldVaultsStrategiesV2.cdc +++ b/cadence/contracts/FlowYieldVaultsStrategiesV2.cdc @@ -382,9 +382,26 @@ access(all) contract FlowYieldVaultsStrategiesV2 { let yieldTokenSource = FlowYieldVaultsAutoBalancers.createExternalSource(id: self.id()!) ?? panic("Could not create external source from AutoBalancer") - // Step 5: Retrieve yield→MOET swapper - let yieldToMoetSwapper = FlowYieldVaultsStrategiesV2._getYieldToMoetSwapper(self.uniqueID!.id) - ?? panic("No yield→MOET swapper found for strategy \(self.id()!)") + // Step 5: Reconstruct yield→MOET swapper from stored CollateralConfig. + // The swapper is no longer cached in the config dict (too expensive to write as + // the debtToCollateralSwappers partition grows on mainnet). Reconstruct instead. + // + // For stablecoin path (e.g. PYUSD0 collateral → internal MOET collateral), + // _getOriginalCollateralType gives us the user-facing key for CollateralConfig lookup. + let closeCollateralKey = self.uniqueID != nil + ? FlowYieldVaultsStrategiesV2._getOriginalCollateralType(self.uniqueID!.id) ?? collateralType + : collateralType + let closeCollateralConfig = FlowYieldVaultsStrategiesV2._getStoredCollateralConfig( + strategyType: Type<@FUSDEVStrategy>(), + collateralType: closeCollateralKey + ) ?? panic("No CollateralConfig for FUSDEVStrategy with \(closeCollateralKey.identifier)") + let closeTokens = FlowYieldVaultsStrategiesV2._resolveTokenBundle( + yieldTokenEVMAddress: closeCollateralConfig.yieldTokenEVMAddress + ) + let yieldToMoetSwapper = FlowYieldVaultsStrategiesV2._buildYieldToDebtSwapper( + tokens: closeTokens, + uniqueID: self.uniqueID! + ) // Step 6: Pre-supplement from collateral if yield is insufficient to cover the full debt. // @@ -405,7 +422,24 @@ access(all) contract FlowYieldVaultsStrategiesV2 { ? yieldToMoetSwapper.quoteOut(forProvided: yieldAvail, reverse: false).outAmount : 0.0 if expectedMOET < totalDebtAmount { - if let collateralToMoetSwapper = FlowYieldVaultsStrategiesV2._getCollateralToDebtSwapper(self.uniqueID!.id) { + if FlowYieldVaultsStrategiesV2._getOriginalCollateralType(self.uniqueID!.id) != nil { + // Stablecoin path: internal collateral IS MOET — pull directly, 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) + } + } else { + // Standard path: reconstruct collateral→MOET swapper from CollateralConfig. + let collateralToMoetSwapper = FlowYieldVaultsStrategiesV2._buildCollateralToDebtSwapper( + collateralConfig: closeCollateralConfig, + tokens: closeTokens, + collateralType: collateralType, + uniqueID: self.uniqueID! + ) let shortfall = totalDebtAmount - expectedMOET // Add 1% buffer to account for swap slippage/rounding in the collateral→MOET leg let buffered = shortfall + shortfall / 100.0 @@ -415,7 +449,6 @@ access(all) contract FlowYieldVaultsStrategiesV2 { 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) @@ -424,21 +457,6 @@ access(all) contract FlowYieldVaultsStrategiesV2 { Burner.burn(<-extraCollateral) } } - } else { - if let id = self.uniqueID { - if FlowYieldVaultsStrategiesV2._getOriginalCollateralType(id.id) != nil { - // Stablecoin pre-swap case: position collateral IS MOET (same as debt token). - // Pull MOET directly from the collateral source to pre-reduce debt — no swap needed. - let shortfall = totalDebtAmount - expectedMOET - let buffered = shortfall + shortfall / 100.0 - let extraMOET <- self.source.withdrawAvailable(maxAmount: buffered) - if extraMOET.balance > 0.0 { - self.position.deposit(from: <-extraMOET) - } else { - Burner.burn(<-extraMOET) - } - } - } } } @@ -458,8 +476,16 @@ access(all) contract FlowYieldVaultsStrategiesV2 { // the collateral vault and optionally a MOET overpayment dust vault. // closePosition returns vaults in dict-iteration order (hash-based), so we cannot // assume the collateral vault is first. Find it by type and convert any non-collateral - // vaults (MOET overpayment dust) back to collateral via the stored swapper. - let debtToCollateralSwapper = FlowYieldVaultsStrategiesV2._getDebtToCollateralSwapper(self.uniqueID!.id) + // vaults (MOET overpayment dust) back to collateral via reconstructed swapper. + // + // Standard path: reconstruct MOET→YIELD→collateral from CollateralConfig. + // Stablecoin path: reconstruct MOET→PYUSD0 via reversed preswap path. + let debtToCollateralSwapper = FlowYieldVaultsStrategiesV2._resolveDebtToCollateralSwapper( + uniqueID: self.uniqueID!, + collateralConfig: closeCollateralConfig, + tokens: closeTokens, + collateralType: collateralType + ) var collateralVault <- DeFiActionsUtils.getEmptyVault(collateralType) while resultVaults.length > 0 { @@ -758,7 +784,7 @@ access(all) contract FlowYieldVaultsStrategiesV2 { if dustVault.getType() == internalCollateralType { collateralVault.deposit(from: <-dustVault) } else { - // Quote first — if dust is too small to route, destroy it + // 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) @@ -1081,9 +1107,6 @@ 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.). @@ -1204,26 +1227,6 @@ access(all) contract FlowYieldVaultsStrategiesV2 { balancerIO.autoBalancer.setSink(positionSwapSink, updateSinkID: true) balancerIO.autoBalancer.setSource(positionSwapSource, updateSourceID: true) - // Store collateral→MOET swapper for pre-supplement in closePosition. - // Used to cover the ~0.02% round-trip fee shortfall when yield hasn't accrued. - let collateralToDebtSwapper = self._createCollateralToDebtSwapper( - collateralConfig: collateralConfig, - tokens: tokens, - collateralType: collateralType, - uniqueID: uniqueID - ) - FlowYieldVaultsStrategiesV2._setCollateralToDebtSwapper(uniqueID.id, collateralToDebtSwapper) - - // Store MOET→collateral swapper for dust conversion in closePosition. - // Chain: MOET → FUSDEV (debtToYieldSwapper) → collateral (yieldToCollateralSwapper) - FlowYieldVaultsStrategiesV2._setDebtToCollateralSwapper( - uniqueID.id, - SwapConnectors.SequentialSwapper( - swappers: [debtToYieldSwapper, yieldToCollateralSwapper], - uniqueID: uniqueID - ) - ) - return <-create FUSDEVStrategy( id: uniqueID, collateralType: collateralType, @@ -1827,6 +1830,21 @@ access(all) contract FlowYieldVaultsStrategiesV2 { } } + /// Returns CollateralConfig for the given strategy+collateral, by value (not reference). + /// Called from contract-level _getStoredCollateralConfig to avoid reference-chain issues. + access(all) fun getCollateralConfig( + strategyType: Type, + collateralType: Type + ): CollateralConfig? { + let composerType = Type<@MorphoERC4626StrategyComposer>() + if let p0 = self.configs[composerType] { + if let p1 = p0[strategyType] { + return p1[collateralType] + } + } + return nil + } + access(self) view fun isSupportedComposer(_ type: Type): Bool { return type == Type<@MorphoERC4626StrategyComposer>() || type == Type<@MoreERC4626StrategyComposer>() @@ -2084,9 +2102,171 @@ access(all) contract FlowYieldVaultsStrategiesV2 { } access(contract) fun _removeDebtToCollateralSwapper(_ id: UInt64) { - var partition = FlowYieldVaultsStrategiesV2.config["debtToCollateralSwappers"] as! {UInt64: {DeFiActions.Swapper}}? ?? {} - partition.remove(key: id) - FlowYieldVaultsStrategiesV2.config["debtToCollateralSwappers"] = partition + let partition = FlowYieldVaultsStrategiesV2.config["debtToCollateralSwappers"] as! {UInt64: {DeFiActions.Swapper}}? ?? {} + if partition[id] == nil { return } // guard: skip expensive write for new positions + var p = partition; p.remove(key: id) + FlowYieldVaultsStrategiesV2.config["debtToCollateralSwappers"] = p + } + + // --- Reconstruction helpers --- + // These allow closePosition to rebuild swappers from stored CollateralConfig at close time, + // avoiding the expensive per-position config dict writes during createStrategy. + + /// Reads CollateralConfig from StrategyComposerIssuer, returning a value copy. + access(self) fun _getStoredCollateralConfig( + strategyType: Type, + collateralType: Type + ): CollateralConfig? { + let issuer = FlowYieldVaultsStrategiesV2.account.storage.borrow< + &FlowYieldVaultsStrategiesV2.StrategyComposerIssuer + >(from: FlowYieldVaultsStrategiesV2.IssuerStoragePath) + if issuer == nil { return nil } + return issuer!.getCollateralConfig(strategyType: strategyType, collateralType: collateralType) + } + + /// Builds a UniswapV3 swapper. Contract-level equivalent of MorphoERC4626StrategyComposer._createUniV3Swapper. + access(self) fun _buildUniV3Swapper( + tokenPath: [EVM.EVMAddress], + feePath: [UInt32], + inVault: Type, + outVault: Type, + uniqueID: DeFiActions.UniqueIdentifier + ): UniswapV3SwapConnectors.Swapper { + return UniswapV3SwapConnectors.Swapper( + factoryAddress: FlowYieldVaultsStrategiesV2.univ3FactoryEVMAddress, + routerAddress: FlowYieldVaultsStrategiesV2.univ3RouterEVMAddress, + quoterAddress: FlowYieldVaultsStrategiesV2.univ3QuoterEVMAddress, + tokenPath: tokenPath, + feePath: feePath, + inVault: inVault, + outVault: outVault, + coaCapability: FlowYieldVaultsStrategiesV2._getCOACapability(), + uniqueID: uniqueID + ) + } + + /// Builds a YIELD→MOET MultiSwapper from a TokenBundle. + /// Contract-level equivalent of MorphoERC4626StrategyComposer._createYieldToDebtSwapper. + access(self) fun _buildYieldToDebtSwapper( + tokens: FlowYieldVaultsStrategiesV2.TokenBundle, + uniqueID: DeFiActions.UniqueIdentifier + ): SwapConnectors.MultiSwapper { + let yieldToDebtAMM = FlowYieldVaultsStrategiesV2._buildUniV3Swapper( + tokenPath: [tokens.yieldTokenEVMAddress, tokens.moetTokenEVMAddress], + feePath: [100], + inVault: tokens.yieldTokenType, + outVault: tokens.moetTokenType, + uniqueID: uniqueID + ) + let yieldToUnderlying = MorphoERC4626SwapConnectors.Swapper( + vaultEVMAddress: tokens.yieldTokenEVMAddress, + coa: FlowYieldVaultsStrategiesV2._getCOACapability(), + feeSource: FlowYieldVaultsStrategiesV2._createFeeSource(withID: uniqueID), + uniqueID: uniqueID, + isReversed: true + ) + let underlyingToDebt = FlowYieldVaultsStrategiesV2._buildUniV3Swapper( + tokenPath: [tokens.underlying4626AssetEVMAddress, tokens.moetTokenEVMAddress], + feePath: [100], + inVault: tokens.underlying4626AssetType, + outVault: tokens.moetTokenType, + uniqueID: uniqueID + ) + let seq = SwapConnectors.SequentialSwapper( + swappers: [yieldToUnderlying, underlyingToDebt], + uniqueID: uniqueID + ) + return SwapConnectors.MultiSwapper( + inVault: tokens.yieldTokenType, + outVault: tokens.moetTokenType, + swappers: [yieldToDebtAMM, seq], + uniqueID: uniqueID + ) + } + + /// Builds a collateral→MOET UniV3 swapper from CollateralConfig. + /// Contract-level equivalent of MorphoERC4626StrategyComposer._createCollateralToDebtSwapper. + access(self) fun _buildCollateralToDebtSwapper( + collateralConfig: FlowYieldVaultsStrategiesV2.CollateralConfig, + tokens: FlowYieldVaultsStrategiesV2.TokenBundle, + collateralType: Type, + uniqueID: DeFiActions.UniqueIdentifier + ): UniswapV3SwapConnectors.Swapper { + let yieldToCollPath = collateralConfig.yieldToCollateralUniV3AddressPath + let yieldToCollFees = collateralConfig.yieldToCollateralUniV3FeePath + assert(yieldToCollPath.length >= 2, message: "yieldToCollateral path must have at least 2 elements") + let collateralEVMAddress = yieldToCollPath[yieldToCollPath.length - 1] + let underlyingEVMAddress = tokens.underlying4626AssetEVMAddress + let collateralToUnderlyingFee = yieldToCollFees[yieldToCollFees.length - 1] + return FlowYieldVaultsStrategiesV2._buildUniV3Swapper( + tokenPath: [collateralEVMAddress, underlyingEVMAddress, tokens.moetTokenEVMAddress], + feePath: [collateralToUnderlyingFee, UInt32(100)], + inVault: collateralType, + outVault: tokens.moetTokenType, + uniqueID: uniqueID + ) + } + + /// Resolves the MOET→collateral swapper for closePosition dust handling. + /// Returns the swapper by value to avoid {DeFiActions.Swapper}? declaration issues at call site. + /// Standard path: MOET→YIELD→collateral (SequentialSwapper). + /// Stablecoin path: MOET→original_collateral via reversed preswap path. + access(self) fun _resolveDebtToCollateralSwapper( + uniqueID: DeFiActions.UniqueIdentifier, + collateralConfig: FlowYieldVaultsStrategiesV2.CollateralConfig, + tokens: FlowYieldVaultsStrategiesV2.TokenBundle, + collateralType: Type + ): {DeFiActions.Swapper}? { + if let origType = FlowYieldVaultsStrategiesV2._getOriginalCollateralType(uniqueID.id) { + if let preswapCfg = FlowYieldVaultsStrategiesV2._getMoetPreswapConfig( + composer: Type<@MorphoERC4626StrategyComposer>(), + collateral: origType + ) { + return FlowYieldVaultsStrategiesV2._buildUniV3Swapper( + tokenPath: preswapCfg.collateralToMoetAddressPath.reverse(), + feePath: preswapCfg.collateralToMoetFeePath.reverse(), + inVault: tokens.moetTokenType, + outVault: origType, + uniqueID: uniqueID + ) + } + return nil + } else { + return FlowYieldVaultsStrategiesV2._buildDebtToCollateralSwapper( + collateralConfig: collateralConfig, + tokens: tokens, + collateralType: collateralType, + uniqueID: uniqueID + ) + } + } + + /// Builds a MOET→collateral SequentialSwapper for dust handling in closePosition. + /// Chain: MOET → YIELD (AMM direct) → collateral (AMM via yieldToCollateral path). + access(self) fun _buildDebtToCollateralSwapper( + collateralConfig: FlowYieldVaultsStrategiesV2.CollateralConfig, + tokens: FlowYieldVaultsStrategiesV2.TokenBundle, + collateralType: Type, + uniqueID: DeFiActions.UniqueIdentifier + ): SwapConnectors.SequentialSwapper { + let debtToYieldAMM = FlowYieldVaultsStrategiesV2._buildUniV3Swapper( + tokenPath: [tokens.moetTokenEVMAddress, tokens.yieldTokenEVMAddress], + feePath: [100], + inVault: tokens.moetTokenType, + outVault: tokens.yieldTokenType, + uniqueID: uniqueID + ) + let yieldToCollateral = FlowYieldVaultsStrategiesV2._buildUniV3Swapper( + tokenPath: collateralConfig.yieldToCollateralUniV3AddressPath, + feePath: collateralConfig.yieldToCollateralUniV3FeePath, + inVault: tokens.yieldTokenType, + outVault: collateralType, + uniqueID: uniqueID + ) + return SwapConnectors.SequentialSwapper( + swappers: [debtToYieldAMM, yieldToCollateral], + uniqueID: uniqueID + ) } // --- "collateralToDebtSwappers" partition --- From a9aaecfc9c874bc73f2d1f961b4c7903081dd051 Mon Sep 17 00:00:00 2001 From: Alex <12097569+nialexsan@users.noreply.github.com> Date: Tue, 17 Mar 2026 15:12:44 -0400 Subject: [PATCH 32/72] buffer swapper for syWFLOWv --- .../contracts/FlowYieldVaultsStrategiesV2.cdc | 84 ++++++++++++------- 1 file changed, 56 insertions(+), 28 deletions(-) diff --git a/cadence/contracts/FlowYieldVaultsStrategiesV2.cdc b/cadence/contracts/FlowYieldVaultsStrategiesV2.cdc index 00fd1a20..a1c22ad4 100644 --- a/cadence/contracts/FlowYieldVaultsStrategiesV2.cdc +++ b/cadence/contracts/FlowYieldVaultsStrategiesV2.cdc @@ -756,45 +756,73 @@ access(all) contract FlowYieldVaultsStrategiesV2 { 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( + // Step 4: Create a BufferedSwapSource that converts ALL syWFLOWv → FLOW for debt repayment. + // Pulls all available tokens (not quoteIn-limited) to avoid ERC4626 rounding underestimates + // that would leave us short of the required FLOW debt. Any FLOW overpayment is returned as + // dust and converted back to collateral below. + let flowSource = BufferedSwapSource( 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)" - ) + // Step 5: Pre-supplement from collateral if yield tokens are insufficient to cover the FLOW debt. + // + // The syWFLOWv close path has a structural round-trip fee loss: + // Open: FLOW → syWFLOWv (ERC4626 deposit, free) + // Close: syWFLOWv → FLOW (UniV3 AMM swap, ~0.3% fee) + // In production, accrued yield more than covers this; with no accrued yield (e.g. in + // tests, immediate open+close), the yield tokens convert back to slightly less FLOW + // than was borrowed. We handle this by pre-pulling a tiny amount of collateral from + // self.source, swapping it to FLOW via debtToCollateralSwapper in reverse, and depositing + // it into the position to reduce the outstanding debt — BEFORE calling position.closePosition. + // + // This MUST be done before closePosition because the position is locked during close: + // any attempt to pull from self.source inside a repaymentSource.withdrawAvailable call + // would trigger "Reentrancy: position X is locked". + let expectedFlow = flowSource.minimumAvailable() + if expectedFlow < totalDebtAmount { + let shortfall = totalDebtAmount - expectedFlow + // Add 1% buffer to account for swap slippage/rounding in the collateral→FLOW leg + let buffered = shortfall + shortfall / 100.0 + let quote = self.debtToCollateralSwapper.quoteIn(forDesired: buffered, reverse: true) + if quote.inAmount > 0.0 { + let extraCollateral <- self.source.withdrawAvailable(maxAmount: quote.inAmount) + if extraCollateral.balance > 0.0 { + let extraFlow <- self.debtToCollateralSwapper.swapBack(quote: quote, residual: <-extraCollateral) + if extraFlow.balance > 0.0 { + self.position.deposit(from: <-extraFlow) + } else { + Burner.burn(<-extraFlow) + } + } else { + Burner.burn(<-extraCollateral) + } + } + } - var collateralVault <- resultVaults.removeFirst() - assert( - collateralVault.getType() == internalCollateralType, - message: "First vault returned from closePosition must be internal collateral (\(internalCollateralType.identifier)), got \(collateralVault.getType().identifier)" - ) + // Step 6: Close position — pool pulls the (now pre-reduced) FLOW debt from flowSource + let resultVaults <- self.position.closePosition(repaymentSources: [flowSource]) - // Handle any overpayment dust (FLOW) returned as the second vault. + // closePosition returns vaults in dict-iteration order (hash-based), so we cannot + // assume the collateral vault is first. Iterate all vaults: collect collateral by type + // and convert any non-collateral vaults (FLOW overpayment dust) back to collateral. + var collateralVault <- DeFiActionsUtils.getEmptyVault(internalCollateralType) while resultVaults.length > 0 { - let dustVault <- resultVaults.removeFirst() - if dustVault.balance > 0.0 { - if dustVault.getType() == internalCollateralType { - collateralVault.deposit(from: <-dustVault) + let v <- resultVaults.removeFirst() + if v.getType() == internalCollateralType { + collateralVault.deposit(from: <-v) + } else if v.balance > 0.0 { + // FLOW overpayment dust — convert back to collateral if routable + let quote = self.debtToCollateralSwapper.quoteOut(forProvided: v.balance, reverse: false) + if quote.outAmount > 0.0 { + let swapped <- self.debtToCollateralSwapper.swap(quote: quote, inVault: <-v) + collateralVault.deposit(from: <-swapped) } else { - // Quote first — if dust is too small to route, destroy it - let quote = self.debtToCollateralSwapper.quoteOut(forProvided: dustVault.balance, reverse: false) - if quote.outAmount > 0.0 { - let swapped <- self.debtToCollateralSwapper.swap(quote: quote, inVault: <-dustVault) - collateralVault.deposit(from: <-swapped) - } else { - Burner.burn(<-dustVault) - } + Burner.burn(<-v) } } else { - Burner.burn(<-dustVault) + Burner.burn(<-v) } } From 3c346a51192bf7d89beb50ecd0f6fb124b69891b Mon Sep 17 00:00:00 2001 From: Alex <12097569+nialexsan@users.noreply.github.com> Date: Tue, 17 Mar 2026 16:17:18 -0400 Subject: [PATCH 33/72] syWFLOWv changes --- .../contracts/FlowYieldVaultsStrategiesV2.cdc | 181 ++++++++++++------ 1 file changed, 119 insertions(+), 62 deletions(-) diff --git a/cadence/contracts/FlowYieldVaultsStrategiesV2.cdc b/cadence/contracts/FlowYieldVaultsStrategiesV2.cdc index a1c22ad4..615e693b 100644 --- a/cadence/contracts/FlowYieldVaultsStrategiesV2.cdc +++ b/cadence/contracts/FlowYieldVaultsStrategiesV2.cdc @@ -45,7 +45,6 @@ access(all) contract FlowYieldVaultsStrategiesV2 { /// Partitioned config map. Each key is a partition name; each value is a typed nested map keyed by /// strategy UniqueIdentifier ID (UInt64). Current partitions: - /// "yieldToMoetSwappers" → {UInt64: {DeFiActions.Swapper}} /// "debtToCollateralSwappers" → {UInt64: {DeFiActions.Swapper}} /// "collateralToDebtSwappers" → {UInt64: {DeFiActions.Swapper}} /// "closedPositions" → {UInt64: Bool} @@ -557,9 +556,11 @@ access(all) contract FlowYieldVaultsStrategiesV2 { 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). + /// Kept for Cadence upgrade compatibility — not read by closePosition. + /// closePosition rebuilds swappers from MoreERC4626CollateralConfig at close time. access(self) let yieldToDebtSwapper: {DeFiActions.Swapper} - /// Swapper used in closePosition to convert FLOW overpayment dust back to collateral (FLOW → collateral via UniV3). + /// Kept for Cadence upgrade compatibility — not read by closePosition. + /// closePosition rebuilds swappers from MoreERC4626CollateralConfig at close time. 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 @@ -752,21 +753,50 @@ access(all) contract FlowYieldVaultsStrategiesV2 { return <- collateralVault } - // Step 3: Create external syWFLOWv source from AutoBalancer + // Step 3: Reconstruct MoreERC4626CollateralConfig and swappers from contract-level config. + // The swappers are not read from the stored resource fields (yieldToDebtSwapper / + // debtToCollateralSwapper); instead they are rebuilt at close time from the shared + // per-strategy config — the same pattern used by FUSDEVStrategy.closePosition. + // + // For the stablecoin path (e.g. PYUSD0 → MOET pre-swap), _getOriginalCollateralType + // gives the user-facing key for the MoreERC4626CollateralConfig lookup. + let closeCollateralKey = self.uniqueID != nil + ? FlowYieldVaultsStrategiesV2._getOriginalCollateralType(self.uniqueID!.id) ?? collateralType + : collateralType + let closeConfig = FlowYieldVaultsStrategiesV2._getStoredMoreERC4626Config( + strategyType: Type<@syWFLOWvStrategy>(), + collateralType: closeCollateralKey + ) ?? panic("No MoreERC4626CollateralConfig for syWFLOWvStrategy with \(closeCollateralKey.identifier)") + let closeTokens = FlowYieldVaultsStrategiesV2._resolveTokenBundle( + yieldTokenEVMAddress: closeConfig.yieldTokenEVMAddress + ) + let syWFLOWvToFlow = FlowYieldVaultsStrategiesV2._buildSyWFLOWvToFlowSwapper( + closeConfig: closeConfig, + closeTokens: closeTokens, + uniqueID: self.uniqueID! + ) + let flowToCollateral = FlowYieldVaultsStrategiesV2._buildSyWFLOWvFlowToCollateralSwapper( + closeConfig: closeConfig, + closeTokens: closeTokens, + internalCollateralType: internalCollateralType, + uniqueID: self.uniqueID! + ) + + // Step 4: 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 BufferedSwapSource that converts ALL syWFLOWv → FLOW for debt repayment. + // Step 5: Create a BufferedSwapSource that converts ALL syWFLOWv → FLOW for debt repayment. // Pulls all available tokens (not quoteIn-limited) to avoid ERC4626 rounding underestimates // that would leave us short of the required FLOW debt. Any FLOW overpayment is returned as // dust and converted back to collateral below. let flowSource = BufferedSwapSource( - swapper: self.yieldToDebtSwapper, + swapper: syWFLOWvToFlow, source: yieldTokenSource, uniqueID: self.copyID() ) - // Step 5: Pre-supplement from collateral if yield tokens are insufficient to cover the FLOW debt. + // Step 6: Pre-supplement from collateral if yield tokens are insufficient to cover the FLOW debt. // // The syWFLOWv close path has a structural round-trip fee loss: // Open: FLOW → syWFLOWv (ERC4626 deposit, free) @@ -774,8 +804,8 @@ access(all) contract FlowYieldVaultsStrategiesV2 { // In production, accrued yield more than covers this; with no accrued yield (e.g. in // tests, immediate open+close), the yield tokens convert back to slightly less FLOW // than was borrowed. We handle this by pre-pulling a tiny amount of collateral from - // self.source, swapping it to FLOW via debtToCollateralSwapper in reverse, and depositing - // it into the position to reduce the outstanding debt — BEFORE calling position.closePosition. + // self.source, swapping it to FLOW via flowToCollateral in reverse, and depositing it + // into the position to reduce the outstanding debt — BEFORE calling position.closePosition. // // This MUST be done before closePosition because the position is locked during close: // any attempt to pull from self.source inside a repaymentSource.withdrawAvailable call @@ -785,11 +815,11 @@ access(all) contract FlowYieldVaultsStrategiesV2 { let shortfall = totalDebtAmount - expectedFlow // Add 1% buffer to account for swap slippage/rounding in the collateral→FLOW leg let buffered = shortfall + shortfall / 100.0 - let quote = self.debtToCollateralSwapper.quoteIn(forDesired: buffered, reverse: true) + let quote = flowToCollateral.quoteIn(forDesired: buffered, reverse: true) if quote.inAmount > 0.0 { let extraCollateral <- self.source.withdrawAvailable(maxAmount: quote.inAmount) if extraCollateral.balance > 0.0 { - let extraFlow <- self.debtToCollateralSwapper.swapBack(quote: quote, residual: <-extraCollateral) + let extraFlow <- flowToCollateral.swapBack(quote: quote, residual: <-extraCollateral) if extraFlow.balance > 0.0 { self.position.deposit(from: <-extraFlow) } else { @@ -801,7 +831,7 @@ access(all) contract FlowYieldVaultsStrategiesV2 { } } - // Step 6: Close position — pool pulls the (now pre-reduced) FLOW debt from flowSource + // Step 7: Close position — pool pulls the (now pre-reduced) FLOW debt from flowSource let resultVaults <- self.position.closePosition(repaymentSources: [flowSource]) // closePosition returns vaults in dict-iteration order (hash-based), so we cannot @@ -814,9 +844,9 @@ access(all) contract FlowYieldVaultsStrategiesV2 { collateralVault.deposit(from: <-v) } else if v.balance > 0.0 { // FLOW overpayment dust — convert back to collateral if routable - let quote = self.debtToCollateralSwapper.quoteOut(forProvided: v.balance, reverse: false) + let quote = flowToCollateral.quoteOut(forProvided: v.balance, reverse: false) if quote.outAmount > 0.0 { - let swapped <- self.debtToCollateralSwapper.swap(quote: quote, inVault: <-v) + let swapped <- flowToCollateral.swap(quote: quote, inVault: <-v) collateralVault.deposit(from: <-swapped) } else { Burner.burn(<-v) @@ -1661,26 +1691,7 @@ 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. - // Path: FLOW → collateral (debtToCollateral) → MOET (pre-swap reversed hop). - // e.g. for PYUSD0: WFLOW→(fee 500)→PYUSD0→(fee 100)→MOET (2-hop). - var flowToMoetPath = collateralConfig.debtToCollateralUniV3AddressPath - flowToMoetPath.append(preswapCfg.collateralToMoetAddressPath[preswapCfg.collateralToMoetAddressPath.length - 1]) - var flowToMoetFees = collateralConfig.debtToCollateralUniV3FeePath - flowToMoetFees.append(preswapCfg.collateralToMoetFeePath[preswapCfg.collateralToMoetFeePath.length - 1]) - let flowToMoet = UniswapV3SwapConnectors.Swapper( - factoryAddress: FlowYieldVaultsStrategiesV2.univ3FactoryEVMAddress, - routerAddress: FlowYieldVaultsStrategiesV2.univ3RouterEVMAddress, - quoterAddress: FlowYieldVaultsStrategiesV2.univ3QuoterEVMAddress, - tokenPath: flowToMoetPath, - feePath: flowToMoetFees, - inVault: tokens.underlying4626AssetType, // FLOW - outVault: tokens.moetTokenType, // MOET - coaCapability: FlowYieldVaultsStrategiesV2._getCOACapability(), - uniqueID: uniqueID - ) - - // MOET→collateral (e.g. PYUSD0): final conversion in closePosition + // MOET→collateral (e.g. PYUSD0): final conversion in closePosition (no-debt path) let moetToCollateral = UniswapV3SwapConnectors.Swapper( factoryAddress: FlowYieldVaultsStrategiesV2.univ3FactoryEVMAddress, routerAddress: FlowYieldVaultsStrategiesV2.univ3RouterEVMAddress, @@ -1703,7 +1714,7 @@ access(all) contract FlowYieldVaultsStrategiesV2 { collateralType: tokens.moetTokenType, position: <-positionPreswap, yieldToDebtSwapper: syWFLOWvToFlow, - debtToCollateralSwapper: flowToMoet + debtToCollateralSwapper: syWFLOWvToFlow // field unused at close time; rebuilt from config ) } @@ -1741,19 +1752,6 @@ access(all) contract FlowYieldVaultsStrategiesV2 { 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) @@ -1762,7 +1760,7 @@ access(all) contract FlowYieldVaultsStrategiesV2 { collateralType: collateralType, position: <-positionFlow, yieldToDebtSwapper: syWFLOWvToFlow, - debtToCollateralSwapper: flowToCollateral + debtToCollateralSwapper: syWFLOWvToFlow // field unused at close time; rebuilt from config ) default: @@ -2103,19 +2101,6 @@ access(all) contract FlowYieldVaultsStrategiesV2 { ) } - // --- "yieldToMoetSwappers" partition --- - - access(contract) view fun _getYieldToMoetSwapper(_ id: UInt64): {DeFiActions.Swapper}? { - let partition = FlowYieldVaultsStrategiesV2.config["yieldToMoetSwappers"] as! {UInt64: {DeFiActions.Swapper}}? ?? {} - return partition[id] - } - - access(contract) fun _setYieldToMoetSwapper(_ id: UInt64, _ swapper: {DeFiActions.Swapper}) { - var partition = FlowYieldVaultsStrategiesV2.config["yieldToMoetSwappers"] as! {UInt64: {DeFiActions.Swapper}}? ?? {} - partition[id] = swapper - FlowYieldVaultsStrategiesV2.config["yieldToMoetSwappers"] = partition - } - // --- "debtToCollateralSwappers" partition --- access(contract) view fun _getDebtToCollateralSwapper(_ id: UInt64): {DeFiActions.Swapper}? { @@ -2297,6 +2282,78 @@ access(all) contract FlowYieldVaultsStrategiesV2 { ) } + // --- syWFLOWvStrategy reconstruction helpers --- + // Parallel to _getStoredCollateralConfig / _buildYieldToDebtSwapper / _resolveDebtToCollateralSwapper + // for FUSDEVStrategy. Allow closePosition to rebuild swappers from stored MoreERC4626CollateralConfig + // at close time rather than using per-position resource fields. + + /// Reads MoreERC4626CollateralConfig from contract-level config, returning a value copy. + /// Parallel to _getStoredCollateralConfig for FUSDEVStrategy/MorphoERC4626Strategy. + access(self) fun _getStoredMoreERC4626Config( + strategyType: Type, + collateralType: Type + ): MoreERC4626CollateralConfig? { + return FlowYieldVaultsStrategiesV2._getMoreERC4626Config( + composer: Type<@MoreERC4626StrategyComposer>(), + strategy: strategyType, + collateral: collateralType + ) + } + + /// Builds a syWFLOWv→FLOW UniV3 swapper from MoreERC4626CollateralConfig. + /// Parallel to _buildYieldToDebtSwapper for MorphoERC4626Strategy. + access(self) fun _buildSyWFLOWvToFlowSwapper( + closeConfig: FlowYieldVaultsStrategiesV2.MoreERC4626CollateralConfig, + closeTokens: FlowYieldVaultsStrategiesV2.TokenBundle, + uniqueID: DeFiActions.UniqueIdentifier + ): UniswapV3SwapConnectors.Swapper { + return FlowYieldVaultsStrategiesV2._buildUniV3Swapper( + tokenPath: closeConfig.yieldToUnderlyingUniV3AddressPath, + feePath: closeConfig.yieldToUnderlyingUniV3FeePath, + inVault: closeTokens.yieldTokenType, + outVault: closeTokens.underlying4626AssetType, // FlowToken.Vault + uniqueID: uniqueID + ) + } + + /// Builds a FLOW→internalCollateral UniV3 swapper from MoreERC4626CollateralConfig. + /// Handles both the standard path (FLOW→WBTC/WETH) and the stablecoin path (FLOW→PYUSD0→MOET). + /// Parallel to _resolveDebtToCollateralSwapper for MorphoERC4626Strategy. + access(self) fun _buildSyWFLOWvFlowToCollateralSwapper( + closeConfig: FlowYieldVaultsStrategiesV2.MoreERC4626CollateralConfig, + closeTokens: FlowYieldVaultsStrategiesV2.TokenBundle, + internalCollateralType: Type, + uniqueID: DeFiActions.UniqueIdentifier + ): UniswapV3SwapConnectors.Swapper { + // Stablecoin path (e.g. PYUSD0→MOET pre-swap): extend the FLOW→PYUSD0 path with a PYUSD0→MOET hop. + if let origType = FlowYieldVaultsStrategiesV2._getOriginalCollateralType(uniqueID.id) { + if let preswapCfg = FlowYieldVaultsStrategiesV2._getMoetPreswapConfig( + composer: Type<@MoreERC4626StrategyComposer>(), + collateral: origType + ) { + var path = closeConfig.debtToCollateralUniV3AddressPath + path.append(preswapCfg.collateralToMoetAddressPath[preswapCfg.collateralToMoetAddressPath.length - 1]) + var fees = closeConfig.debtToCollateralUniV3FeePath + fees.append(preswapCfg.collateralToMoetFeePath[preswapCfg.collateralToMoetFeePath.length - 1]) + return FlowYieldVaultsStrategiesV2._buildUniV3Swapper( + tokenPath: path, + feePath: fees, + inVault: closeTokens.underlying4626AssetType, // FLOW + outVault: internalCollateralType, // MOET + uniqueID: uniqueID + ) + } + } + // Standard path (WBTC/WETH): FLOW → collateral directly. + return FlowYieldVaultsStrategiesV2._buildUniV3Swapper( + tokenPath: closeConfig.debtToCollateralUniV3AddressPath, + feePath: closeConfig.debtToCollateralUniV3FeePath, + inVault: closeTokens.underlying4626AssetType, // FLOW + outVault: internalCollateralType, + uniqueID: uniqueID + ) + } + // --- "collateralToDebtSwappers" partition --- // Stores a collateral→debt (collateral→PYUSD0→MOET) UniV3 swapper per FUSDEVStrategy uniqueID. // Used in FUSDEVStrategy.closePosition to pre-supplement the debt when yield tokens alone are From a0ed98c5b07df2279ee0827be59ec7b67fa8c68c Mon Sep 17 00:00:00 2001 From: Alex <12097569+nialexsan@users.noreply.github.com> Date: Tue, 17 Mar 2026 23:33:11 -0400 Subject: [PATCH 34/72] remove preswap for fusdev --- .../contracts/FlowYieldVaultsStrategiesV2.cdc | 232 +++--------------- 1 file changed, 32 insertions(+), 200 deletions(-) diff --git a/cadence/contracts/FlowYieldVaultsStrategiesV2.cdc b/cadence/contracts/FlowYieldVaultsStrategiesV2.cdc index 615e693b..52a1dbf1 100644 --- a/cadence/contracts/FlowYieldVaultsStrategiesV2.cdc +++ b/cadence/contracts/FlowYieldVaultsStrategiesV2.cdc @@ -239,59 +239,19 @@ access(all) contract FlowYieldVaultsStrategiesV2 { // access(all) view fun isSupportedCollateralType(_ type: Type): Bool access(all) view fun getSupportedCollateralTypes(): {Type: Bool} { - // If this strategy was initialized with a stablecoin pre-swap (e.g. PYUSD0→MOET), - // expose the original (external) collateral type to callers, not the internal MOET type. - if let id = self.uniqueID { - if let originalType = FlowYieldVaultsStrategiesV2._getOriginalCollateralType(id.id) { - return { originalType: true } - } - } return { self.sink.getSinkType(): true } } /// Returns the amount available for withdrawal via the inner Source access(all) fun availableBalance(ofToken: Type): UFix64 { if FlowYieldVaultsStrategiesV2._isPositionClosed(self.uniqueID) { return 0.0 } - // If stablecoin pre-swap is in effect, match against the original (external) collateral type. - // MOET and PYUSD0 are both stablecoins with approximately equal value (1:1), so the MOET - // balance is a reasonable approximation of the PYUSD0-denominated collateral balance. - var effectiveSourceType = self.source.getSourceType() - if let id = self.uniqueID { - if let originalType = FlowYieldVaultsStrategiesV2._getOriginalCollateralType(id.id) { - effectiveSourceType = originalType - } - } - return ofToken == effectiveSourceType ? self.source.minimumAvailable() : 0.0 + return ofToken == self.source.getSourceType() ? self.source.minimumAvailable() : 0.0 } /// Deposits up to the inner Sink's capacity from the provided authorized Vault reference. - /// Accepts both the internal collateral type (MOET) and, when a pre-swap is configured, - /// the original external collateral type (e.g. PYUSD0) — which is swapped to MOET first. access(all) fun deposit(from: auth(FungibleToken.Withdraw) &{FungibleToken.Vault}) { pre { - from.getType() == self.sink.getSinkType() - || (self.uniqueID != nil && FlowYieldVaultsStrategiesV2._getOriginalCollateralType(self.uniqueID!.id) == from.getType()): + from.getType() == self.sink.getSinkType(): "FUSDEVStrategy position only accepts \(self.sink.getSinkType().identifier) as collateral, got \(from.getType().identifier)" } - // 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, @@ -357,22 +317,6 @@ access(all) contract FlowYieldVaultsStrategiesV2 { } var collateralVault <- resultVaults.removeFirst() destroy resultVaults - // Convert internal collateral (MOET) → external collateral (e.g. PYUSD0) if needed - if let id = self.uniqueID { - if let moetToOrigSwapper = FlowYieldVaultsStrategiesV2._getMoetToCollateralSwapper(id.id) { - if collateralVault.balance > 0.0 { - let quote = moetToOrigSwapper.quoteOut(forProvided: collateralVault.balance, reverse: false) - if quote.outAmount > 0.0 { - let extVault <- moetToOrigSwapper.swap(quote: quote, inVault: <-collateralVault) - FlowYieldVaultsStrategiesV2._markPositionClosed(self.uniqueID) - return <- extVault - } - } - Burner.burn(<-collateralVault) - FlowYieldVaultsStrategiesV2._markPositionClosed(self.uniqueID) - return <- DeFiActionsUtils.getEmptyVault(collateralType) - } - } FlowYieldVaultsStrategiesV2._markPositionClosed(self.uniqueID) return <- collateralVault } @@ -384,16 +328,10 @@ access(all) contract FlowYieldVaultsStrategiesV2 { // Step 5: Reconstruct yield→MOET swapper from stored CollateralConfig. // The swapper is no longer cached in the config dict (too expensive to write as // the debtToCollateralSwappers partition grows on mainnet). Reconstruct instead. - // - // For stablecoin path (e.g. PYUSD0 collateral → internal MOET collateral), - // _getOriginalCollateralType gives us the user-facing key for CollateralConfig lookup. - let closeCollateralKey = self.uniqueID != nil - ? FlowYieldVaultsStrategiesV2._getOriginalCollateralType(self.uniqueID!.id) ?? collateralType - : collateralType let closeCollateralConfig = FlowYieldVaultsStrategiesV2._getStoredCollateralConfig( strategyType: Type<@FUSDEVStrategy>(), - collateralType: closeCollateralKey - ) ?? panic("No CollateralConfig for FUSDEVStrategy with \(closeCollateralKey.identifier)") + collateralType: collateralType + ) ?? panic("No CollateralConfig for FUSDEVStrategy with \(collateralType.identifier)") let closeTokens = FlowYieldVaultsStrategiesV2._resolveTokenBundle( yieldTokenEVMAddress: closeCollateralConfig.yieldTokenEVMAddress ) @@ -421,40 +359,27 @@ access(all) contract FlowYieldVaultsStrategiesV2 { ? yieldToMoetSwapper.quoteOut(forProvided: yieldAvail, reverse: false).outAmount : 0.0 if expectedMOET < totalDebtAmount { - if FlowYieldVaultsStrategiesV2._getOriginalCollateralType(self.uniqueID!.id) != nil { - // Stablecoin path: internal collateral IS MOET — pull directly, 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) - } - } else { - // Standard path: reconstruct collateral→MOET swapper from CollateralConfig. - let collateralToMoetSwapper = FlowYieldVaultsStrategiesV2._buildCollateralToDebtSwapper( - collateralConfig: closeCollateralConfig, - tokens: closeTokens, - collateralType: collateralType, - uniqueID: self.uniqueID! - ) - let shortfall = totalDebtAmount - expectedMOET - // Add 1% buffer to account for swap slippage/rounding in the collateral→MOET leg - let buffered = shortfall + shortfall / 100.0 - let quote = collateralToMoetSwapper.quoteIn(forDesired: buffered, reverse: false) - if quote.inAmount > 0.0 { - let extraCollateral <- self.source.withdrawAvailable(maxAmount: quote.inAmount) - if extraCollateral.balance > 0.0 { - let extraMOET <- collateralToMoetSwapper.swap(quote: quote, inVault: <-extraCollateral) - if extraMOET.balance > 0.0 { - self.position.deposit(from: <-extraMOET) - } else { - Burner.burn(<-extraMOET) - } + let collateralToMoetSwapper = FlowYieldVaultsStrategiesV2._buildCollateralToDebtSwapper( + collateralConfig: closeCollateralConfig, + tokens: closeTokens, + collateralType: collateralType, + uniqueID: self.uniqueID! + ) + let shortfall = totalDebtAmount - expectedMOET + // Add 1% buffer to account for swap slippage/rounding in the collateral→MOET leg + let buffered = shortfall + shortfall / 100.0 + let quote = collateralToMoetSwapper.quoteIn(forDesired: buffered, reverse: false) + if quote.inAmount > 0.0 { + let extraCollateral <- self.source.withdrawAvailable(maxAmount: quote.inAmount) + if extraCollateral.balance > 0.0 { + let extraMOET <- collateralToMoetSwapper.swap(quote: quote, inVault: <-extraCollateral) + if extraMOET.balance > 0.0 { + self.position.deposit(from: <-extraMOET) } else { - Burner.burn(<-extraCollateral) + Burner.burn(<-extraMOET) } + } else { + Burner.burn(<-extraCollateral) } } } @@ -476,9 +401,7 @@ access(all) contract FlowYieldVaultsStrategiesV2 { // closePosition returns vaults in dict-iteration order (hash-based), so we cannot // assume the collateral vault is first. Find it by type and convert any non-collateral // vaults (MOET overpayment dust) back to collateral via reconstructed swapper. - // - // Standard path: reconstruct MOET→YIELD→collateral from CollateralConfig. - // Stablecoin path: reconstruct MOET→PYUSD0 via reversed preswap path. + // Reconstruct MOET→YIELD→collateral from CollateralConfig. let debtToCollateralSwapper = FlowYieldVaultsStrategiesV2._resolveDebtToCollateralSwapper( uniqueID: self.uniqueID!, collateralConfig: closeCollateralConfig, @@ -517,11 +440,7 @@ access(all) contract FlowYieldVaultsStrategiesV2 { access(contract) fun burnCallback() { FlowYieldVaultsAutoBalancers._cleanupAutoBalancer(id: self.id()!) FlowYieldVaultsStrategiesV2._cleanupPositionClosed(self.uniqueID) - // Clean up stablecoin pre-swap config entries (no-op if not set) if let id = self.uniqueID { - FlowYieldVaultsStrategiesV2._removeOriginalCollateralType(id.id) - FlowYieldVaultsStrategiesV2._removeCollateralPreSwapper(id.id) - FlowYieldVaultsStrategiesV2._removeMoetToCollateralSwapper(id.id) FlowYieldVaultsStrategiesV2._removeDebtToCollateralSwapper(id.id) } } @@ -1165,76 +1084,7 @@ access(all) contract FlowYieldVaultsStrategiesV2 { uniqueID: uniqueID ) - // --- 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) --- + // --- Standard path (WBTC, WETH, WFLOW — directly supported by FlowALP) --- // Open FlowALPv0 position let position <- FlowYieldVaultsStrategiesV2._openCreditPosition( @@ -2221,37 +2071,19 @@ access(all) contract FlowYieldVaultsStrategiesV2 { } /// Resolves the MOET→collateral swapper for closePosition dust handling. - /// Returns the swapper by value to avoid {DeFiActions.Swapper}? declaration issues at call site. - /// Standard path: MOET→YIELD→collateral (SequentialSwapper). - /// Stablecoin path: MOET→original_collateral via reversed preswap path. + /// Builds MOET→YIELD→collateral (SequentialSwapper) from CollateralConfig. access(self) fun _resolveDebtToCollateralSwapper( uniqueID: DeFiActions.UniqueIdentifier, collateralConfig: FlowYieldVaultsStrategiesV2.CollateralConfig, tokens: FlowYieldVaultsStrategiesV2.TokenBundle, collateralType: Type ): {DeFiActions.Swapper}? { - if let origType = FlowYieldVaultsStrategiesV2._getOriginalCollateralType(uniqueID.id) { - if let preswapCfg = FlowYieldVaultsStrategiesV2._getMoetPreswapConfig( - composer: Type<@MorphoERC4626StrategyComposer>(), - collateral: origType - ) { - return FlowYieldVaultsStrategiesV2._buildUniV3Swapper( - tokenPath: preswapCfg.collateralToMoetAddressPath.reverse(), - feePath: preswapCfg.collateralToMoetFeePath.reverse(), - inVault: tokens.moetTokenType, - outVault: origType, - uniqueID: uniqueID - ) - } - return nil - } else { - return FlowYieldVaultsStrategiesV2._buildDebtToCollateralSwapper( - collateralConfig: collateralConfig, - tokens: tokens, - collateralType: collateralType, - uniqueID: uniqueID - ) - } + return FlowYieldVaultsStrategiesV2._buildDebtToCollateralSwapper( + collateralConfig: collateralConfig, + tokens: tokens, + collateralType: collateralType, + uniqueID: uniqueID + ) } /// Builds a MOET→collateral SequentialSwapper for dust handling in closePosition. From ebc0d44e6475310685b116a6be1db98af2f23fc5 Mon Sep 17 00:00:00 2001 From: Alex <12097569+nialexsan@users.noreply.github.com> Date: Wed, 18 Mar 2026 10:47:39 -0400 Subject: [PATCH 35/72] move methods into strategy --- .../contracts/FlowYieldVaultsStrategiesV2.cdc | 394 +++++++++--------- 1 file changed, 192 insertions(+), 202 deletions(-) diff --git a/cadence/contracts/FlowYieldVaultsStrategiesV2.cdc b/cadence/contracts/FlowYieldVaultsStrategiesV2.cdc index 52a1dbf1..dedd58d4 100644 --- a/cadence/contracts/FlowYieldVaultsStrategiesV2.cdc +++ b/cadence/contracts/FlowYieldVaultsStrategiesV2.cdc @@ -328,14 +328,14 @@ access(all) contract FlowYieldVaultsStrategiesV2 { // Step 5: Reconstruct yield→MOET swapper from stored CollateralConfig. // The swapper is no longer cached in the config dict (too expensive to write as // the debtToCollateralSwappers partition grows on mainnet). Reconstruct instead. - let closeCollateralConfig = FlowYieldVaultsStrategiesV2._getStoredCollateralConfig( + let closeCollateralConfig = self._getStoredCollateralConfig( strategyType: Type<@FUSDEVStrategy>(), collateralType: collateralType ) ?? panic("No CollateralConfig for FUSDEVStrategy with \(collateralType.identifier)") let closeTokens = FlowYieldVaultsStrategiesV2._resolveTokenBundle( yieldTokenEVMAddress: closeCollateralConfig.yieldTokenEVMAddress ) - let yieldToMoetSwapper = FlowYieldVaultsStrategiesV2._buildYieldToDebtSwapper( + let yieldToMoetSwapper = self._buildYieldToDebtSwapper( tokens: closeTokens, uniqueID: self.uniqueID! ) @@ -359,7 +359,7 @@ access(all) contract FlowYieldVaultsStrategiesV2 { ? yieldToMoetSwapper.quoteOut(forProvided: yieldAvail, reverse: false).outAmount : 0.0 if expectedMOET < totalDebtAmount { - let collateralToMoetSwapper = FlowYieldVaultsStrategiesV2._buildCollateralToDebtSwapper( + let collateralToMoetSwapper = self._buildCollateralToDebtSwapper( collateralConfig: closeCollateralConfig, tokens: closeTokens, collateralType: collateralType, @@ -402,7 +402,7 @@ access(all) contract FlowYieldVaultsStrategiesV2 { // assume the collateral vault is first. Find it by type and convert any non-collateral // vaults (MOET overpayment dust) back to collateral via reconstructed swapper. // Reconstruct MOET→YIELD→collateral from CollateralConfig. - let debtToCollateralSwapper = FlowYieldVaultsStrategiesV2._resolveDebtToCollateralSwapper( + let debtToCollateralSwapper = self._resolveDebtToCollateralSwapper( uniqueID: self.uniqueID!, collateralConfig: closeCollateralConfig, tokens: closeTokens, @@ -460,6 +460,123 @@ access(all) contract FlowYieldVaultsStrategiesV2 { access(contract) fun setID(_ id: DeFiActions.UniqueIdentifier?) { self.uniqueID = id } + + /* =========================== + closePosition helpers + =========================== */ + + access(self) fun _getStoredCollateralConfig( + strategyType: Type, + collateralType: Type + ): CollateralConfig? { + let issuer = FlowYieldVaultsStrategiesV2.account.storage.borrow< + &FlowYieldVaultsStrategiesV2.StrategyComposerIssuer + >(from: FlowYieldVaultsStrategiesV2.IssuerStoragePath) + if issuer == nil { return nil } + return issuer!.getCollateralConfig(strategyType: strategyType, collateralType: collateralType) + } + + /// Builds a YIELD→MOET MultiSwapper (AMM direct + ERC4626 redeem path). + access(self) fun _buildYieldToDebtSwapper( + tokens: FlowYieldVaultsStrategiesV2.TokenBundle, + uniqueID: DeFiActions.UniqueIdentifier + ): SwapConnectors.MultiSwapper { + let yieldToDebtAMM = FlowYieldVaultsStrategiesV2._buildUniV3Swapper( + tokenPath: [tokens.yieldTokenEVMAddress, tokens.moetTokenEVMAddress], + feePath: [100], + inVault: tokens.yieldTokenType, + outVault: tokens.moetTokenType, + uniqueID: uniqueID + ) + let yieldToUnderlying = MorphoERC4626SwapConnectors.Swapper( + vaultEVMAddress: tokens.yieldTokenEVMAddress, + coa: FlowYieldVaultsStrategiesV2._getCOACapability(), + feeSource: FlowYieldVaultsStrategiesV2._createFeeSource(withID: uniqueID), + uniqueID: uniqueID, + isReversed: true + ) + let underlyingToDebt = FlowYieldVaultsStrategiesV2._buildUniV3Swapper( + tokenPath: [tokens.underlying4626AssetEVMAddress, tokens.moetTokenEVMAddress], + feePath: [100], + inVault: tokens.underlying4626AssetType, + outVault: tokens.moetTokenType, + uniqueID: uniqueID + ) + let seq = SwapConnectors.SequentialSwapper( + swappers: [yieldToUnderlying, underlyingToDebt], + uniqueID: uniqueID + ) + return SwapConnectors.MultiSwapper( + inVault: tokens.yieldTokenType, + outVault: tokens.moetTokenType, + swappers: [yieldToDebtAMM, seq], + uniqueID: uniqueID + ) + } + + /// Builds a collateral→MOET UniV3 swapper from CollateralConfig. + access(self) fun _buildCollateralToDebtSwapper( + collateralConfig: FlowYieldVaultsStrategiesV2.CollateralConfig, + tokens: FlowYieldVaultsStrategiesV2.TokenBundle, + collateralType: Type, + uniqueID: DeFiActions.UniqueIdentifier + ): UniswapV3SwapConnectors.Swapper { + let yieldToCollPath = collateralConfig.yieldToCollateralUniV3AddressPath + let yieldToCollFees = collateralConfig.yieldToCollateralUniV3FeePath + assert(yieldToCollPath.length >= 2, message: "yieldToCollateral path must have at least 2 elements") + let collateralEVMAddress = yieldToCollPath[yieldToCollPath.length - 1] + let underlyingEVMAddress = tokens.underlying4626AssetEVMAddress + let collateralToUnderlyingFee = yieldToCollFees[yieldToCollFees.length - 1] + return FlowYieldVaultsStrategiesV2._buildUniV3Swapper( + tokenPath: [collateralEVMAddress, underlyingEVMAddress, tokens.moetTokenEVMAddress], + feePath: [collateralToUnderlyingFee, UInt32(100)], + inVault: collateralType, + outVault: tokens.moetTokenType, + uniqueID: uniqueID + ) + } + + /// Builds a MOET→collateral SequentialSwapper for dust handling: MOET→YIELD→collateral. + access(self) fun _buildDebtToCollateralSwapper( + collateralConfig: FlowYieldVaultsStrategiesV2.CollateralConfig, + tokens: FlowYieldVaultsStrategiesV2.TokenBundle, + collateralType: Type, + uniqueID: DeFiActions.UniqueIdentifier + ): SwapConnectors.SequentialSwapper { + let debtToYieldAMM = FlowYieldVaultsStrategiesV2._buildUniV3Swapper( + tokenPath: [tokens.moetTokenEVMAddress, tokens.yieldTokenEVMAddress], + feePath: [100], + inVault: tokens.moetTokenType, + outVault: tokens.yieldTokenType, + uniqueID: uniqueID + ) + let yieldToCollateral = FlowYieldVaultsStrategiesV2._buildUniV3Swapper( + tokenPath: collateralConfig.yieldToCollateralUniV3AddressPath, + feePath: collateralConfig.yieldToCollateralUniV3FeePath, + inVault: tokens.yieldTokenType, + outVault: collateralType, + uniqueID: uniqueID + ) + return SwapConnectors.SequentialSwapper( + swappers: [debtToYieldAMM, yieldToCollateral], + uniqueID: uniqueID + ) + } + + /// Resolves the MOET→collateral swapper for closePosition dust handling. + access(self) fun _resolveDebtToCollateralSwapper( + uniqueID: DeFiActions.UniqueIdentifier, + collateralConfig: FlowYieldVaultsStrategiesV2.CollateralConfig, + tokens: FlowYieldVaultsStrategiesV2.TokenBundle, + collateralType: Type + ): {DeFiActions.Swapper}? { + return self._buildDebtToCollateralSwapper( + collateralConfig: collateralConfig, + tokens: tokens, + collateralType: collateralType, + uniqueID: uniqueID + ) + } } /// This strategy uses syWFLOWv vault (More ERC4626). @@ -682,19 +799,19 @@ access(all) contract FlowYieldVaultsStrategiesV2 { let closeCollateralKey = self.uniqueID != nil ? FlowYieldVaultsStrategiesV2._getOriginalCollateralType(self.uniqueID!.id) ?? collateralType : collateralType - let closeConfig = FlowYieldVaultsStrategiesV2._getStoredMoreERC4626Config( + let closeConfig = self._getStoredMoreERC4626Config( strategyType: Type<@syWFLOWvStrategy>(), collateralType: closeCollateralKey ) ?? panic("No MoreERC4626CollateralConfig for syWFLOWvStrategy with \(closeCollateralKey.identifier)") let closeTokens = FlowYieldVaultsStrategiesV2._resolveTokenBundle( yieldTokenEVMAddress: closeConfig.yieldTokenEVMAddress ) - let syWFLOWvToFlow = FlowYieldVaultsStrategiesV2._buildSyWFLOWvToFlowSwapper( + let syWFLOWvToFlow = self._buildSyWFLOWvToFlowSwapper( closeConfig: closeConfig, closeTokens: closeTokens, uniqueID: self.uniqueID! ) - let flowToCollateral = FlowYieldVaultsStrategiesV2._buildSyWFLOWvFlowToCollateralSwapper( + let flowToCollateral = self._buildSyWFLOWvFlowToCollateralSwapper( closeConfig: closeConfig, closeTokens: closeTokens, internalCollateralType: internalCollateralType, @@ -826,6 +943,73 @@ access(all) contract FlowYieldVaultsStrategiesV2 { access(contract) fun setID(_ id: DeFiActions.UniqueIdentifier?) { self.uniqueID = id } + + /* =========================== + closePosition helpers + =========================== */ + + access(self) fun _getStoredMoreERC4626Config( + strategyType: Type, + collateralType: Type + ): MoreERC4626CollateralConfig? { + return FlowYieldVaultsStrategiesV2._getMoreERC4626Config( + composer: Type<@MoreERC4626StrategyComposer>(), + strategy: strategyType, + collateral: collateralType + ) + } + + /// Builds a syWFLOWv→FLOW UniV3 swapper from MoreERC4626CollateralConfig. + access(self) fun _buildSyWFLOWvToFlowSwapper( + closeConfig: FlowYieldVaultsStrategiesV2.MoreERC4626CollateralConfig, + closeTokens: FlowYieldVaultsStrategiesV2.TokenBundle, + uniqueID: DeFiActions.UniqueIdentifier + ): UniswapV3SwapConnectors.Swapper { + return FlowYieldVaultsStrategiesV2._buildUniV3Swapper( + tokenPath: closeConfig.yieldToUnderlyingUniV3AddressPath, + feePath: closeConfig.yieldToUnderlyingUniV3FeePath, + inVault: closeTokens.yieldTokenType, + outVault: closeTokens.underlying4626AssetType, // FlowToken.Vault + uniqueID: uniqueID + ) + } + + /// Builds a FLOW→internalCollateral UniV3 swapper from MoreERC4626CollateralConfig. + /// Handles both the standard path (FLOW→WBTC/WETH) and the stablecoin path (FLOW→PYUSD0→MOET). + access(self) fun _buildSyWFLOWvFlowToCollateralSwapper( + closeConfig: FlowYieldVaultsStrategiesV2.MoreERC4626CollateralConfig, + closeTokens: FlowYieldVaultsStrategiesV2.TokenBundle, + internalCollateralType: Type, + uniqueID: DeFiActions.UniqueIdentifier + ): UniswapV3SwapConnectors.Swapper { + // Stablecoin path (e.g. PYUSD0→MOET pre-swap): extend the FLOW→PYUSD0 path with a PYUSD0→MOET hop. + if let origType = FlowYieldVaultsStrategiesV2._getOriginalCollateralType(uniqueID.id) { + if let preswapCfg = FlowYieldVaultsStrategiesV2._getMoetPreswapConfig( + composer: Type<@MoreERC4626StrategyComposer>(), + collateral: origType + ) { + var path = closeConfig.debtToCollateralUniV3AddressPath + path.append(preswapCfg.collateralToMoetAddressPath[preswapCfg.collateralToMoetAddressPath.length - 1]) + var fees = closeConfig.debtToCollateralUniV3FeePath + fees.append(preswapCfg.collateralToMoetFeePath[preswapCfg.collateralToMoetFeePath.length - 1]) + return FlowYieldVaultsStrategiesV2._buildUniV3Swapper( + tokenPath: path, + feePath: fees, + inVault: closeTokens.underlying4626AssetType, // FLOW + outVault: internalCollateralType, // MOET + uniqueID: uniqueID + ) + } + } + // Standard path (WBTC/WETH): FLOW → collateral directly. + return FlowYieldVaultsStrategiesV2._buildUniV3Swapper( + tokenPath: closeConfig.debtToCollateralUniV3AddressPath, + feePath: closeConfig.debtToCollateralUniV3FeePath, + inVault: closeTokens.underlying4626AssetType, // FLOW + outVault: internalCollateralType, + uniqueID: uniqueID + ) + } } access(all) struct TokenBundle { @@ -1971,23 +2155,7 @@ access(all) contract FlowYieldVaultsStrategiesV2 { FlowYieldVaultsStrategiesV2.config["debtToCollateralSwappers"] = p } - // --- Reconstruction helpers --- - // These allow closePosition to rebuild swappers from stored CollateralConfig at close time, - // avoiding the expensive per-position config dict writes during createStrategy. - - /// Reads CollateralConfig from StrategyComposerIssuer, returning a value copy. - access(self) fun _getStoredCollateralConfig( - strategyType: Type, - collateralType: Type - ): CollateralConfig? { - let issuer = FlowYieldVaultsStrategiesV2.account.storage.borrow< - &FlowYieldVaultsStrategiesV2.StrategyComposerIssuer - >(from: FlowYieldVaultsStrategiesV2.IssuerStoragePath) - if issuer == nil { return nil } - return issuer!.getCollateralConfig(strategyType: strategyType, collateralType: collateralType) - } - - /// Builds a UniswapV3 swapper. Contract-level equivalent of MorphoERC4626StrategyComposer._createUniV3Swapper. + /// Builds a UniswapV3 swapper. Shared by FUSDEVStrategy and syWFLOWvStrategy closePosition helpers. access(self) fun _buildUniV3Swapper( tokenPath: [EVM.EVMAddress], feePath: [UInt32], @@ -2008,184 +2176,6 @@ access(all) contract FlowYieldVaultsStrategiesV2 { ) } - /// Builds a YIELD→MOET MultiSwapper from a TokenBundle. - /// Contract-level equivalent of MorphoERC4626StrategyComposer._createYieldToDebtSwapper. - access(self) fun _buildYieldToDebtSwapper( - tokens: FlowYieldVaultsStrategiesV2.TokenBundle, - uniqueID: DeFiActions.UniqueIdentifier - ): SwapConnectors.MultiSwapper { - let yieldToDebtAMM = FlowYieldVaultsStrategiesV2._buildUniV3Swapper( - tokenPath: [tokens.yieldTokenEVMAddress, tokens.moetTokenEVMAddress], - feePath: [100], - inVault: tokens.yieldTokenType, - outVault: tokens.moetTokenType, - uniqueID: uniqueID - ) - let yieldToUnderlying = MorphoERC4626SwapConnectors.Swapper( - vaultEVMAddress: tokens.yieldTokenEVMAddress, - coa: FlowYieldVaultsStrategiesV2._getCOACapability(), - feeSource: FlowYieldVaultsStrategiesV2._createFeeSource(withID: uniqueID), - uniqueID: uniqueID, - isReversed: true - ) - let underlyingToDebt = FlowYieldVaultsStrategiesV2._buildUniV3Swapper( - tokenPath: [tokens.underlying4626AssetEVMAddress, tokens.moetTokenEVMAddress], - feePath: [100], - inVault: tokens.underlying4626AssetType, - outVault: tokens.moetTokenType, - uniqueID: uniqueID - ) - let seq = SwapConnectors.SequentialSwapper( - swappers: [yieldToUnderlying, underlyingToDebt], - uniqueID: uniqueID - ) - return SwapConnectors.MultiSwapper( - inVault: tokens.yieldTokenType, - outVault: tokens.moetTokenType, - swappers: [yieldToDebtAMM, seq], - uniqueID: uniqueID - ) - } - - /// Builds a collateral→MOET UniV3 swapper from CollateralConfig. - /// Contract-level equivalent of MorphoERC4626StrategyComposer._createCollateralToDebtSwapper. - access(self) fun _buildCollateralToDebtSwapper( - collateralConfig: FlowYieldVaultsStrategiesV2.CollateralConfig, - tokens: FlowYieldVaultsStrategiesV2.TokenBundle, - collateralType: Type, - uniqueID: DeFiActions.UniqueIdentifier - ): UniswapV3SwapConnectors.Swapper { - let yieldToCollPath = collateralConfig.yieldToCollateralUniV3AddressPath - let yieldToCollFees = collateralConfig.yieldToCollateralUniV3FeePath - assert(yieldToCollPath.length >= 2, message: "yieldToCollateral path must have at least 2 elements") - let collateralEVMAddress = yieldToCollPath[yieldToCollPath.length - 1] - let underlyingEVMAddress = tokens.underlying4626AssetEVMAddress - let collateralToUnderlyingFee = yieldToCollFees[yieldToCollFees.length - 1] - return FlowYieldVaultsStrategiesV2._buildUniV3Swapper( - tokenPath: [collateralEVMAddress, underlyingEVMAddress, tokens.moetTokenEVMAddress], - feePath: [collateralToUnderlyingFee, UInt32(100)], - inVault: collateralType, - outVault: tokens.moetTokenType, - uniqueID: uniqueID - ) - } - - /// Resolves the MOET→collateral swapper for closePosition dust handling. - /// Builds MOET→YIELD→collateral (SequentialSwapper) from CollateralConfig. - access(self) fun _resolveDebtToCollateralSwapper( - uniqueID: DeFiActions.UniqueIdentifier, - collateralConfig: FlowYieldVaultsStrategiesV2.CollateralConfig, - tokens: FlowYieldVaultsStrategiesV2.TokenBundle, - collateralType: Type - ): {DeFiActions.Swapper}? { - return FlowYieldVaultsStrategiesV2._buildDebtToCollateralSwapper( - collateralConfig: collateralConfig, - tokens: tokens, - collateralType: collateralType, - uniqueID: uniqueID - ) - } - - /// Builds a MOET→collateral SequentialSwapper for dust handling in closePosition. - /// Chain: MOET → YIELD (AMM direct) → collateral (AMM via yieldToCollateral path). - access(self) fun _buildDebtToCollateralSwapper( - collateralConfig: FlowYieldVaultsStrategiesV2.CollateralConfig, - tokens: FlowYieldVaultsStrategiesV2.TokenBundle, - collateralType: Type, - uniqueID: DeFiActions.UniqueIdentifier - ): SwapConnectors.SequentialSwapper { - let debtToYieldAMM = FlowYieldVaultsStrategiesV2._buildUniV3Swapper( - tokenPath: [tokens.moetTokenEVMAddress, tokens.yieldTokenEVMAddress], - feePath: [100], - inVault: tokens.moetTokenType, - outVault: tokens.yieldTokenType, - uniqueID: uniqueID - ) - let yieldToCollateral = FlowYieldVaultsStrategiesV2._buildUniV3Swapper( - tokenPath: collateralConfig.yieldToCollateralUniV3AddressPath, - feePath: collateralConfig.yieldToCollateralUniV3FeePath, - inVault: tokens.yieldTokenType, - outVault: collateralType, - uniqueID: uniqueID - ) - return SwapConnectors.SequentialSwapper( - swappers: [debtToYieldAMM, yieldToCollateral], - uniqueID: uniqueID - ) - } - - // --- syWFLOWvStrategy reconstruction helpers --- - // Parallel to _getStoredCollateralConfig / _buildYieldToDebtSwapper / _resolveDebtToCollateralSwapper - // for FUSDEVStrategy. Allow closePosition to rebuild swappers from stored MoreERC4626CollateralConfig - // at close time rather than using per-position resource fields. - - /// Reads MoreERC4626CollateralConfig from contract-level config, returning a value copy. - /// Parallel to _getStoredCollateralConfig for FUSDEVStrategy/MorphoERC4626Strategy. - access(self) fun _getStoredMoreERC4626Config( - strategyType: Type, - collateralType: Type - ): MoreERC4626CollateralConfig? { - return FlowYieldVaultsStrategiesV2._getMoreERC4626Config( - composer: Type<@MoreERC4626StrategyComposer>(), - strategy: strategyType, - collateral: collateralType - ) - } - - /// Builds a syWFLOWv→FLOW UniV3 swapper from MoreERC4626CollateralConfig. - /// Parallel to _buildYieldToDebtSwapper for MorphoERC4626Strategy. - access(self) fun _buildSyWFLOWvToFlowSwapper( - closeConfig: FlowYieldVaultsStrategiesV2.MoreERC4626CollateralConfig, - closeTokens: FlowYieldVaultsStrategiesV2.TokenBundle, - uniqueID: DeFiActions.UniqueIdentifier - ): UniswapV3SwapConnectors.Swapper { - return FlowYieldVaultsStrategiesV2._buildUniV3Swapper( - tokenPath: closeConfig.yieldToUnderlyingUniV3AddressPath, - feePath: closeConfig.yieldToUnderlyingUniV3FeePath, - inVault: closeTokens.yieldTokenType, - outVault: closeTokens.underlying4626AssetType, // FlowToken.Vault - uniqueID: uniqueID - ) - } - - /// Builds a FLOW→internalCollateral UniV3 swapper from MoreERC4626CollateralConfig. - /// Handles both the standard path (FLOW→WBTC/WETH) and the stablecoin path (FLOW→PYUSD0→MOET). - /// Parallel to _resolveDebtToCollateralSwapper for MorphoERC4626Strategy. - access(self) fun _buildSyWFLOWvFlowToCollateralSwapper( - closeConfig: FlowYieldVaultsStrategiesV2.MoreERC4626CollateralConfig, - closeTokens: FlowYieldVaultsStrategiesV2.TokenBundle, - internalCollateralType: Type, - uniqueID: DeFiActions.UniqueIdentifier - ): UniswapV3SwapConnectors.Swapper { - // Stablecoin path (e.g. PYUSD0→MOET pre-swap): extend the FLOW→PYUSD0 path with a PYUSD0→MOET hop. - if let origType = FlowYieldVaultsStrategiesV2._getOriginalCollateralType(uniqueID.id) { - if let preswapCfg = FlowYieldVaultsStrategiesV2._getMoetPreswapConfig( - composer: Type<@MoreERC4626StrategyComposer>(), - collateral: origType - ) { - var path = closeConfig.debtToCollateralUniV3AddressPath - path.append(preswapCfg.collateralToMoetAddressPath[preswapCfg.collateralToMoetAddressPath.length - 1]) - var fees = closeConfig.debtToCollateralUniV3FeePath - fees.append(preswapCfg.collateralToMoetFeePath[preswapCfg.collateralToMoetFeePath.length - 1]) - return FlowYieldVaultsStrategiesV2._buildUniV3Swapper( - tokenPath: path, - feePath: fees, - inVault: closeTokens.underlying4626AssetType, // FLOW - outVault: internalCollateralType, // MOET - uniqueID: uniqueID - ) - } - } - // Standard path (WBTC/WETH): FLOW → collateral directly. - return FlowYieldVaultsStrategiesV2._buildUniV3Swapper( - tokenPath: closeConfig.debtToCollateralUniV3AddressPath, - feePath: closeConfig.debtToCollateralUniV3FeePath, - inVault: closeTokens.underlying4626AssetType, // FLOW - outVault: internalCollateralType, - uniqueID: uniqueID - ) - } - // --- "collateralToDebtSwappers" partition --- // Stores a collateral→debt (collateral→PYUSD0→MOET) UniV3 swapper per FUSDEVStrategy uniqueID. // Used in FUSDEVStrategy.closePosition to pre-supplement the debt when yield tokens alone are From 97483fd7a7ea69214eee49c0e88226d9514f9c6b Mon Sep 17 00:00:00 2001 From: Alex <12097569+nialexsan@users.noreply.github.com> Date: Wed, 18 Mar 2026 11:30:19 -0400 Subject: [PATCH 36/72] refactor sywflowv --- .../contracts/FlowYieldVaultsStrategiesV2.cdc | 164 ++++++------------ 1 file changed, 52 insertions(+), 112 deletions(-) diff --git a/cadence/contracts/FlowYieldVaultsStrategiesV2.cdc b/cadence/contracts/FlowYieldVaultsStrategiesV2.cdc index dedd58d4..f06d5494 100644 --- a/cadence/contracts/FlowYieldVaultsStrategiesV2.cdc +++ b/cadence/contracts/FlowYieldVaultsStrategiesV2.cdc @@ -45,15 +45,11 @@ 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: - /// "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 @@ -326,8 +322,6 @@ access(all) contract FlowYieldVaultsStrategiesV2 { ?? panic("Could not create external source from AutoBalancer") // Step 5: Reconstruct yield→MOET swapper from stored CollateralConfig. - // The swapper is no longer cached in the config dict (too expensive to write as - // the debtToCollateralSwappers partition grows on mainnet). Reconstruct instead. let closeCollateralConfig = self._getStoredCollateralConfig( strategyType: Type<@FUSDEVStrategy>(), collateralType: collateralType @@ -440,9 +434,6 @@ access(all) contract FlowYieldVaultsStrategiesV2 { access(contract) fun burnCallback() { FlowYieldVaultsAutoBalancers._cleanupAutoBalancer(id: self.id()!) FlowYieldVaultsStrategiesV2._cleanupPositionClosed(self.uniqueID) - if let id = self.uniqueID { - FlowYieldVaultsStrategiesV2._removeDebtToCollateralSwapper(id.id) - } } access(all) fun getComponentInfo(): DeFiActions.ComponentInfo { return DeFiActions.ComponentInfo( @@ -666,7 +657,7 @@ access(all) contract FlowYieldVaultsStrategiesV2 { // 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) { + if let preSwapper = self._buildCollateralToMoetSwapper(uniqueID: id) { let incoming <- from.withdraw(amount: from.balance) if incoming.balance > 0.0 { let quote = preSwapper.quoteOut(forProvided: incoming.balance, reverse: false) @@ -698,7 +689,7 @@ access(all) contract FlowYieldVaultsStrategiesV2 { if let id = self.uniqueID { if let originalType = FlowYieldVaultsStrategiesV2._getOriginalCollateralType(id.id) { if ofToken == originalType { - if let moetToOrigSwapper = FlowYieldVaultsStrategiesV2._getMoetToCollateralSwapper(id.id) { + if let moetToOrigSwapper = self._buildMoetToCollateralSwapper(uniqueID: id) { // Quote MOET in to get maxAmount PYUSD0 out let quote = moetToOrigSwapper.quoteIn(forDesired: maxAmount, reverse: false) if quote.inAmount > 0.0 { @@ -767,7 +758,7 @@ access(all) contract FlowYieldVaultsStrategiesV2 { // 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 let moetToOrigSwapper = self._buildMoetToCollateralSwapper(uniqueID: id) { if collateralVault.balance > 0.0 { let quote = moetToOrigSwapper.quoteOut(forProvided: collateralVault.balance, reverse: false) if quote.outAmount > 0.0 { @@ -811,7 +802,7 @@ access(all) contract FlowYieldVaultsStrategiesV2 { closeTokens: closeTokens, uniqueID: self.uniqueID! ) - let flowToCollateral = self._buildSyWFLOWvFlowToCollateralSwapper( + let flowToCollateral = self._buildFlowToCollateralSwapper( closeConfig: closeConfig, closeTokens: closeTokens, internalCollateralType: internalCollateralType, @@ -897,7 +888,7 @@ access(all) contract FlowYieldVaultsStrategiesV2 { // 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 let moetToOrigSwapper = self._buildMoetToCollateralSwapper(uniqueID: id) { if collateralVault.balance > 0.0 { let quote = moetToOrigSwapper.quoteOut(forProvided: collateralVault.balance, reverse: false) if quote.outAmount > 0.0 { @@ -923,8 +914,6 @@ access(all) contract FlowYieldVaultsStrategiesV2 { // Clean up stablecoin pre-swap config entries (no-op if not set) if let id = self.uniqueID { FlowYieldVaultsStrategiesV2._removeOriginalCollateralType(id.id) - FlowYieldVaultsStrategiesV2._removeCollateralPreSwapper(id.id) - FlowYieldVaultsStrategiesV2._removeMoetToCollateralSwapper(id.id) } } access(all) fun getComponentInfo(): DeFiActions.ComponentInfo { @@ -974,9 +963,55 @@ access(all) contract FlowYieldVaultsStrategiesV2 { ) } + /// Builds a collateral→MOET UniV3 swapper from MoetPreswapConfig for the deposit pre-swap. + /// Returns nil if no preswap is configured for this position (non-stablecoin collateral). + access(self) fun _buildCollateralToMoetSwapper( + uniqueID: DeFiActions.UniqueIdentifier + ): UniswapV3SwapConnectors.Swapper? { + if let origType = FlowYieldVaultsStrategiesV2._getOriginalCollateralType(uniqueID.id) { + if let preswapCfg = FlowYieldVaultsStrategiesV2._getMoetPreswapConfig( + composer: Type<@MoreERC4626StrategyComposer>(), + collateral: origType + ) { + let moetType = FlowYieldVaultsStrategiesV2._getPoolDefaultToken() + return FlowYieldVaultsStrategiesV2._buildUniV3Swapper( + tokenPath: preswapCfg.collateralToMoetAddressPath, + feePath: preswapCfg.collateralToMoetFeePath, + inVault: origType, + outVault: moetType, + uniqueID: uniqueID + ) + } + } + return nil + } + + /// Builds a MOET→originalCollateral UniV3 swapper from MoetPreswapConfig (reversed path). + /// Returns nil if no preswap is configured for this position (non-stablecoin collateral). + access(self) fun _buildMoetToCollateralSwapper( + uniqueID: DeFiActions.UniqueIdentifier + ): UniswapV3SwapConnectors.Swapper? { + if let origType = FlowYieldVaultsStrategiesV2._getOriginalCollateralType(uniqueID.id) { + if let preswapCfg = FlowYieldVaultsStrategiesV2._getMoetPreswapConfig( + composer: Type<@MoreERC4626StrategyComposer>(), + collateral: origType + ) { + let moetType = FlowYieldVaultsStrategiesV2._getPoolDefaultToken() + return FlowYieldVaultsStrategiesV2._buildUniV3Swapper( + tokenPath: preswapCfg.collateralToMoetAddressPath.reverse(), + feePath: preswapCfg.collateralToMoetFeePath.reverse(), + inVault: moetType, + outVault: origType, + uniqueID: uniqueID + ) + } + } + return nil + } + /// Builds a FLOW→internalCollateral UniV3 swapper from MoreERC4626CollateralConfig. /// Handles both the standard path (FLOW→WBTC/WETH) and the stablecoin path (FLOW→PYUSD0→MOET). - access(self) fun _buildSyWFLOWvFlowToCollateralSwapper( + access(self) fun _buildFlowToCollateralSwapper( closeConfig: FlowYieldVaultsStrategiesV2.MoreERC4626CollateralConfig, closeTokens: FlowYieldVaultsStrategiesV2.TokenBundle, internalCollateralType: Type, @@ -1725,23 +1760,8 @@ access(all) contract FlowYieldVaultsStrategiesV2 { balancerIO.autoBalancer.setSink(positionDebtSwapSinkPre, updateSinkID: true) balancerIO.autoBalancer.setSource(positionDebtSwapSourcePre, updateSourceID: true) - // MOET→collateral (e.g. PYUSD0): final conversion in closePosition (no-debt path) - 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, @@ -2135,26 +2155,6 @@ access(all) contract FlowYieldVaultsStrategiesV2 { ) } - // --- "debtToCollateralSwappers" partition --- - - access(contract) view fun _getDebtToCollateralSwapper(_ id: UInt64): {DeFiActions.Swapper}? { - let partition = FlowYieldVaultsStrategiesV2.config["debtToCollateralSwappers"] as! {UInt64: {DeFiActions.Swapper}}? ?? {} - return partition[id] - } - - access(contract) fun _setDebtToCollateralSwapper(_ id: UInt64, _ swapper: {DeFiActions.Swapper}) { - var partition = FlowYieldVaultsStrategiesV2.config["debtToCollateralSwappers"] as! {UInt64: {DeFiActions.Swapper}}? ?? {} - partition[id] = swapper - FlowYieldVaultsStrategiesV2.config["debtToCollateralSwappers"] = partition - } - - access(contract) fun _removeDebtToCollateralSwapper(_ id: UInt64) { - let partition = FlowYieldVaultsStrategiesV2.config["debtToCollateralSwappers"] as! {UInt64: {DeFiActions.Swapper}}? ?? {} - if partition[id] == nil { return } // guard: skip expensive write for new positions - var p = partition; p.remove(key: id) - FlowYieldVaultsStrategiesV2.config["debtToCollateralSwappers"] = p - } - /// Builds a UniswapV3 swapper. Shared by FUSDEVStrategy and syWFLOWvStrategy closePosition helpers. access(self) fun _buildUniV3Swapper( tokenPath: [EVM.EVMAddress], @@ -2176,22 +2176,6 @@ access(all) contract FlowYieldVaultsStrategiesV2 { ) } - // --- "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 { @@ -2307,50 +2291,6 @@ access(all) contract FlowYieldVaultsStrategiesV2 { FlowYieldVaultsStrategiesV2.config["originalCollateralTypes"] = partition } - // --- "collateralPreSwappers" partition --- - // Stores a collateral→MOET swapper per strategy uniqueID. - // Used in deposit() to pre-swap incoming stablecoin collateral (e.g. PYUSD0) to MOET. - - access(contract) view fun _getCollateralPreSwapper(_ id: UInt64): {DeFiActions.Swapper}? { - let partition = FlowYieldVaultsStrategiesV2.config["collateralPreSwappers"] as! {UInt64: {DeFiActions.Swapper}}? ?? {} - return partition[id] - } - - access(contract) fun _setCollateralPreSwapper(_ id: UInt64, _ swapper: {DeFiActions.Swapper}) { - var partition = FlowYieldVaultsStrategiesV2.config["collateralPreSwappers"] as! {UInt64: {DeFiActions.Swapper}}? ?? {} - partition[id] = swapper - FlowYieldVaultsStrategiesV2.config["collateralPreSwappers"] = partition - } - - access(contract) fun _removeCollateralPreSwapper(_ id: UInt64) { - var partition = FlowYieldVaultsStrategiesV2.config["collateralPreSwappers"] as! {UInt64: {DeFiActions.Swapper}}? ?? {} - partition.remove(key: id) - FlowYieldVaultsStrategiesV2.config["collateralPreSwappers"] = partition - } - - // --- "moetToCollateralSwappers" partition --- - // Stores a MOET→original-collateral swapper per strategy uniqueID (FUSDEVStrategy 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. From bb6fafaa52acbc00a54b26c053254a73b1af5d1f Mon Sep 17 00:00:00 2001 From: Alex <12097569+nialexsan@users.noreply.github.com> Date: Wed, 18 Mar 2026 13:09:00 -0400 Subject: [PATCH 37/72] fixes from branch --- .../contracts/FlowYieldVaultsStrategiesV2.cdc | 39 ++++++------------- 1 file changed, 11 insertions(+), 28 deletions(-) diff --git a/cadence/contracts/FlowYieldVaultsStrategiesV2.cdc b/cadence/contracts/FlowYieldVaultsStrategiesV2.cdc index f06d5494..e0b406e6 100644 --- a/cadence/contracts/FlowYieldVaultsStrategiesV2.cdc +++ b/cadence/contracts/FlowYieldVaultsStrategiesV2.cdc @@ -396,11 +396,11 @@ access(all) contract FlowYieldVaultsStrategiesV2 { // assume the collateral vault is first. Find it by type and convert any non-collateral // vaults (MOET overpayment dust) back to collateral via reconstructed swapper. // Reconstruct MOET→YIELD→collateral from CollateralConfig. - let debtToCollateralSwapper = self._resolveDebtToCollateralSwapper( - uniqueID: self.uniqueID!, + let debtToCollateralSwapper = self._buildDebtToCollateralSwapper( collateralConfig: closeCollateralConfig, tokens: closeTokens, - collateralType: collateralType + collateralType: collateralType, + uniqueID: self.uniqueID! ) var collateralVault <- DeFiActionsUtils.getEmptyVault(collateralType) @@ -409,15 +409,11 @@ access(all) contract FlowYieldVaultsStrategiesV2 { if v.getType() == collateralType { collateralVault.deposit(from: <-v) } else if v.balance > 0.0 { - if let swapper = debtToCollateralSwapper { - // Quote first — if dust is too small to route, destroy it - let quote = swapper.quoteOut(forProvided: v.balance, reverse: false) - if quote.outAmount > 0.0 { - let swapped <- swapper.swap(quote: quote, inVault: <-v) - collateralVault.deposit(from: <-swapped) - } else { - Burner.burn(<-v) - } + // Quote first — if dust is too small to route, destroy it + let quote = debtToCollateralSwapper.quoteOut(forProvided: v.balance, reverse: false) + if quote.outAmount > 0.0 { + let swapped <- debtToCollateralSwapper.swap(quote: quote, inVault: <-v) + collateralVault.deposit(from: <-swapped) } else { Burner.burn(<-v) } @@ -553,21 +549,6 @@ access(all) contract FlowYieldVaultsStrategiesV2 { uniqueID: uniqueID ) } - - /// Resolves the MOET→collateral swapper for closePosition dust handling. - access(self) fun _resolveDebtToCollateralSwapper( - uniqueID: DeFiActions.UniqueIdentifier, - collateralConfig: FlowYieldVaultsStrategiesV2.CollateralConfig, - tokens: FlowYieldVaultsStrategiesV2.TokenBundle, - collateralType: Type - ): {DeFiActions.Swapper}? { - return self._buildDebtToCollateralSwapper( - collateralConfig: collateralConfig, - tokens: tokens, - collateralType: collateralType, - uniqueID: uniqueID - ) - } } /// This strategy uses syWFLOWv vault (More ERC4626). @@ -1936,7 +1917,9 @@ access(all) contract FlowYieldVaultsStrategiesV2 { } switch type { case Type<@MorphoERC4626StrategyComposer>(): - return <- create MorphoERC4626StrategyComposer(self.configs[type] ?? {}) + return <- create MorphoERC4626StrategyComposer( + self.configs[type] ?? panic("No config registered for \(type.identifier)") + ) case Type<@MoreERC4626StrategyComposer>(): return <- create MoreERC4626StrategyComposer( FlowYieldVaultsStrategiesV2._getMoreERC4626ComposerConfig(type) From fdc178e323affa4ccc1fe409a52e0fe0ddfe4257 Mon Sep 17 00:00:00 2001 From: Alex <12097569+nialexsan@users.noreply.github.com> Date: Wed, 18 Mar 2026 13:29:39 -0400 Subject: [PATCH 38/72] init params validation --- .../contracts/FlowYieldVaultsStrategiesV2.cdc | 36 ++++++++----------- 1 file changed, 15 insertions(+), 21 deletions(-) diff --git a/cadence/contracts/FlowYieldVaultsStrategiesV2.cdc b/cadence/contracts/FlowYieldVaultsStrategiesV2.cdc index e0b406e6..e0f97509 100644 --- a/cadence/contracts/FlowYieldVaultsStrategiesV2.cdc +++ b/cadence/contracts/FlowYieldVaultsStrategiesV2.cdc @@ -199,6 +199,18 @@ access(all) contract FlowYieldVaultsStrategiesV2 { debtToCollateralUniV3AddressPath: [EVM.EVMAddress], debtToCollateralUniV3FeePath: [UInt32] ) { + pre { + yieldToUnderlyingUniV3AddressPath.length > 1: + "Invalid yieldToUnderlying UniV3 path length" + yieldToUnderlyingUniV3FeePath.length == yieldToUnderlyingUniV3AddressPath.length - 1: + "Invalid yieldToUnderlying UniV3 fee path length" + yieldToUnderlyingUniV3AddressPath[0].equals(yieldTokenEVMAddress): + "yieldToUnderlying UniV3 path must start with yield token" + debtToCollateralUniV3AddressPath.length > 1: + "Invalid debtToCollateral UniV3 path length" + debtToCollateralUniV3FeePath.length == debtToCollateralUniV3AddressPath.length - 1: + "Invalid debtToCollateral UniV3 fee path length" + } self.yieldTokenEVMAddress = yieldTokenEVMAddress self.yieldToUnderlyingUniV3AddressPath = yieldToUnderlyingUniV3AddressPath self.yieldToUnderlyingUniV3FeePath = yieldToUnderlyingUniV3FeePath @@ -563,30 +575,16 @@ 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 - /// Kept for Cadence upgrade compatibility — not read by closePosition. - /// closePosition rebuilds swappers from MoreERC4626CollateralConfig at close time. - access(self) let yieldToDebtSwapper: {DeFiActions.Swapper} - /// Kept for Cadence upgrade compatibility — not read by closePosition. - /// closePosition rebuilds swappers from MoreERC4626CollateralConfig at close time. - access(self) let debtToCollateralSwapper: {DeFiActions.Swapper} - /// Tracks whether the underlying FlowALP position has been closed. Once true, - /// availableBalance() returns 0.0 to avoid panicking when the pool no longer - /// holds the position (e.g. during YieldVault burnCallback after close). access(self) var positionClosed: Bool init( id: DeFiActions.UniqueIdentifier, collateralType: Type, - position: @FlowALPv0.Position, - yieldToDebtSwapper: {DeFiActions.Swapper}, - debtToCollateralSwapper: {DeFiActions.Swapper} + position: @FlowALPv0.Position ) { 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 } @@ -1747,9 +1745,7 @@ access(all) contract FlowYieldVaultsStrategiesV2 { return <-create syWFLOWvStrategy( id: uniqueID, collateralType: tokens.moetTokenType, - position: <-positionPreswap, - yieldToDebtSwapper: syWFLOWvToFlow, - debtToCollateralSwapper: syWFLOWvToFlow // field unused at close time; rebuilt from config + position: <-positionPreswap ) } @@ -1793,9 +1789,7 @@ access(all) contract FlowYieldVaultsStrategiesV2 { return <-create syWFLOWvStrategy( id: uniqueID, collateralType: collateralType, - position: <-positionFlow, - yieldToDebtSwapper: syWFLOWvToFlow, - debtToCollateralSwapper: syWFLOWvToFlow // field unused at close time; rebuilt from config + position: <-positionFlow ) default: From 4596b21c83dda227d1aaa01dba9522f8d55e7671 Mon Sep 17 00:00:00 2001 From: Alex <12097569+nialexsan@users.noreply.github.com> Date: Wed, 18 Mar 2026 14:52:24 -0400 Subject: [PATCH 39/72] fix reverse path --- .../contracts/FlowYieldVaultsStrategiesV2.cdc | 86 +++++++++++-------- 1 file changed, 48 insertions(+), 38 deletions(-) diff --git a/cadence/contracts/FlowYieldVaultsStrategiesV2.cdc b/cadence/contracts/FlowYieldVaultsStrategiesV2.cdc index e0f97509..3938c420 100644 --- a/cadence/contracts/FlowYieldVaultsStrategiesV2.cdc +++ b/cadence/contracts/FlowYieldVaultsStrategiesV2.cdc @@ -251,7 +251,7 @@ access(all) contract FlowYieldVaultsStrategiesV2 { } /// Returns the amount available for withdrawal via the inner Source access(all) fun availableBalance(ofToken: Type): UFix64 { - if FlowYieldVaultsStrategiesV2._isPositionClosed(self.uniqueID) { return 0.0 } + if self._isPositionClosed() { return 0.0 } return ofToken == self.source.getSourceType() ? self.source.minimumAvailable() : 0.0 } /// Deposits up to the inner Sink's capacity from the provided authorized Vault reference. @@ -320,12 +320,12 @@ access(all) contract FlowYieldVaultsStrategiesV2 { // Zero vaults: dust collateral rounded down to zero — return an empty vault if resultVaults.length == 0 { destroy resultVaults - FlowYieldVaultsStrategiesV2._markPositionClosed(self.uniqueID) + self._markPositionClosed() return <- DeFiActionsUtils.getEmptyVault(collateralType) } var collateralVault <- resultVaults.removeFirst() destroy resultVaults - FlowYieldVaultsStrategiesV2._markPositionClosed(self.uniqueID) + self._markPositionClosed() return <- collateralVault } @@ -435,13 +435,13 @@ access(all) contract FlowYieldVaultsStrategiesV2 { } destroy resultVaults - FlowYieldVaultsStrategiesV2._markPositionClosed(self.uniqueID) + self._markPositionClosed() return <- collateralVault } /// Executed when a Strategy is burned, cleaning up the Strategy's stored AutoBalancer access(contract) fun burnCallback() { FlowYieldVaultsAutoBalancers._cleanupAutoBalancer(id: self.id()!) - FlowYieldVaultsStrategiesV2._cleanupPositionClosed(self.uniqueID) + self._cleanupPositionClosed() } access(all) fun getComponentInfo(): DeFiActions.ComponentInfo { return DeFiActions.ComponentInfo( @@ -514,6 +514,9 @@ access(all) contract FlowYieldVaultsStrategiesV2 { } /// Builds a collateral→MOET UniV3 swapper from CollateralConfig. + /// Derives the path by reversing yieldToCollateralUniV3AddressPath[1..] (skipping the + /// yield token) and appending MOET, preserving all intermediate hops. + /// e.g. [FUSDEV, PYUSD0, WETH, WBTC] → [WBTC, WETH, PYUSD0, MOET] access(self) fun _buildCollateralToDebtSwapper( collateralConfig: FlowYieldVaultsStrategiesV2.CollateralConfig, tokens: FlowYieldVaultsStrategiesV2.TokenBundle, @@ -523,12 +526,23 @@ access(all) contract FlowYieldVaultsStrategiesV2 { let yieldToCollPath = collateralConfig.yieldToCollateralUniV3AddressPath let yieldToCollFees = collateralConfig.yieldToCollateralUniV3FeePath assert(yieldToCollPath.length >= 2, message: "yieldToCollateral path must have at least 2 elements") - let collateralEVMAddress = yieldToCollPath[yieldToCollPath.length - 1] - let underlyingEVMAddress = tokens.underlying4626AssetEVMAddress - let collateralToUnderlyingFee = yieldToCollFees[yieldToCollFees.length - 1] + // Build reversed path: iterate yieldToCollPath from last down to index 1 (skip yield token at 0), + // then append MOET. e.g. [FUSDEV, PYUSD0, WETH, WBTC] → [WBTC, WETH, PYUSD0] + MOET + var collToDebtPath: [EVM.EVMAddress] = [] + var collToDebtFees: [UInt32] = [] + for i in InclusiveRange(yieldToCollPath.length - 1, 1, step: -1) { + collToDebtPath.append(yieldToCollPath[i]) + } + collToDebtPath.append(tokens.moetTokenEVMAddress) + // Build reversed fees: iterate from last down to index 1 (skip yield→underlying fee at 0), + // then append PYUSD0→MOET fee (100). e.g. [100, 3000, 3000] → [3000, 3000] + 100 + for i in InclusiveRange(yieldToCollFees.length - 1, 1, step: -1) { + collToDebtFees.append(yieldToCollFees[i]) + } + collToDebtFees.append(UInt32(100)) return FlowYieldVaultsStrategiesV2._buildUniV3Swapper( - tokenPath: [collateralEVMAddress, underlyingEVMAddress, tokens.moetTokenEVMAddress], - feePath: [collateralToUnderlyingFee, UInt32(100)], + tokenPath: collToDebtPath, + feePath: collToDebtFees, inVault: collateralType, outVault: tokens.moetTokenType, uniqueID: uniqueID @@ -561,6 +575,30 @@ access(all) contract FlowYieldVaultsStrategiesV2 { uniqueID: uniqueID ) } + + access(self) view fun _isPositionClosed(): Bool { + if let id = self.uniqueID { + let partition = FlowYieldVaultsStrategiesV2.config["closedPositions"] as! {UInt64: Bool}? ?? {} + return partition[id.id] ?? false + } + return false + } + + access(self) fun _markPositionClosed() { + if let id = self.uniqueID { + var partition = FlowYieldVaultsStrategiesV2.config["closedPositions"] as! {UInt64: Bool}? ?? {} + partition[id.id] = true + FlowYieldVaultsStrategiesV2.config["closedPositions"] = partition + } + } + + access(self) fun _cleanupPositionClosed() { + if let id = self.uniqueID { + var partition = FlowYieldVaultsStrategiesV2.config["closedPositions"] as! {UInt64: Bool}? ?? {} + partition.remove(key: id.id) + FlowYieldVaultsStrategiesV2.config["closedPositions"] = partition + } + } } /// This strategy uses syWFLOWv vault (More ERC4626). @@ -760,10 +798,6 @@ access(all) contract FlowYieldVaultsStrategiesV2 { } // Step 3: Reconstruct MoreERC4626CollateralConfig and swappers from contract-level config. - // The swappers are not read from the stored resource fields (yieldToDebtSwapper / - // debtToCollateralSwapper); instead they are rebuilt at close time from the shared - // per-strategy config — the same pattern used by FUSDEVStrategy.closePosition. - // // For the stablecoin path (e.g. PYUSD0 → MOET pre-swap), _getOriginalCollateralType // gives the user-facing key for the MoreERC4626CollateralConfig lookup. let closeCollateralKey = self.uniqueID != nil @@ -2153,30 +2187,6 @@ access(all) contract FlowYieldVaultsStrategiesV2 { ) } - // --- "closedPositions" partition --- - - access(contract) view fun _isPositionClosed(_ uniqueID: DeFiActions.UniqueIdentifier?): Bool { - if uniqueID == nil { return false } - let partition = FlowYieldVaultsStrategiesV2.config["closedPositions"] as! {UInt64: Bool}? ?? {} - return partition[uniqueID!.id] ?? false - } - - access(contract) fun _markPositionClosed(_ uniqueID: DeFiActions.UniqueIdentifier?) { - if let id = uniqueID { - var partition = FlowYieldVaultsStrategiesV2.config["closedPositions"] as! {UInt64: Bool}? ?? {} - partition[id.id] = true - FlowYieldVaultsStrategiesV2.config["closedPositions"] = partition - } - } - - access(contract) fun _cleanupPositionClosed(_ uniqueID: DeFiActions.UniqueIdentifier?) { - if let id = uniqueID { - var partition = FlowYieldVaultsStrategiesV2.config["closedPositions"] as! {UInt64: Bool}? ?? {} - partition.remove(key: id.id) - FlowYieldVaultsStrategiesV2.config["closedPositions"] = partition - } - } - // --- "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. From bf73100eac4ba1556e13c2c1294b295ef3a2c81e Mon Sep 17 00:00:00 2001 From: Alex <12097569+nialexsan@users.noreply.github.com> Date: Wed, 18 Mar 2026 15:45:33 -0400 Subject: [PATCH 40/72] additional safeguards --- .../contracts/FlowYieldVaultsStrategiesV2.cdc | 120 ++++++++---------- 1 file changed, 56 insertions(+), 64 deletions(-) diff --git a/cadence/contracts/FlowYieldVaultsStrategiesV2.cdc b/cadence/contracts/FlowYieldVaultsStrategiesV2.cdc index 3938c420..9b5eef35 100644 --- a/cadence/contracts/FlowYieldVaultsStrategiesV2.cdc +++ b/cadence/contracts/FlowYieldVaultsStrategiesV2.cdc @@ -118,7 +118,9 @@ access(all) contract FlowYieldVaultsStrategiesV2 { Burner.burn(<-sourceLiquidity) return <- DeFiActionsUtils.getEmptyVault(self.getSourceType()) } - return <- self.swapper.swap(quote: nil, inVault: <-sourceLiquidity) + let swapped <- self.swapper.swap(quote: nil, inVault: <-sourceLiquidity) + assert(swapped.balance > 0.0, message: "BufferedSwapSource: swap returned zero despite available input") + return <- swapped } } @@ -210,6 +212,8 @@ access(all) contract FlowYieldVaultsStrategiesV2 { "Invalid debtToCollateral UniV3 path length" debtToCollateralUniV3FeePath.length == debtToCollateralUniV3AddressPath.length - 1: "Invalid debtToCollateral UniV3 fee path length" + debtToCollateralUniV3AddressPath[0].equals(yieldToUnderlyingUniV3AddressPath[yieldToUnderlyingUniV3AddressPath.length - 1]): + "debtToCollateral UniV3 path must start with the underlying asset (end of yieldToUnderlying path)" } self.yieldTokenEVMAddress = yieldTokenEVMAddress self.yieldToUnderlyingUniV3AddressPath = yieldToUnderlyingUniV3AddressPath @@ -377,16 +381,12 @@ access(all) contract FlowYieldVaultsStrategiesV2 { let quote = collateralToMoetSwapper.quoteIn(forDesired: buffered, reverse: false) if quote.inAmount > 0.0 { let extraCollateral <- self.source.withdrawAvailable(maxAmount: quote.inAmount) - if extraCollateral.balance > 0.0 { - let extraMOET <- collateralToMoetSwapper.swap(quote: quote, inVault: <-extraCollateral) - if extraMOET.balance > 0.0 { - self.position.deposit(from: <-extraMOET) - } else { - Burner.burn(<-extraMOET) - } - } else { - Burner.burn(<-extraCollateral) - } + assert(extraCollateral.balance > 0.0, + message: "Pre-supplement: no collateral available to cover shortfall of \(shortfall) MOET") + let extraMOET <- collateralToMoetSwapper.swap(quote: quote, inVault: <-extraCollateral) + assert(extraMOET.balance > 0.0, + message: "Pre-supplement: collateral→MOET swap produced zero output") + self.position.deposit(from: <-extraMOET) } } @@ -678,13 +678,11 @@ access(all) contract FlowYieldVaultsStrategiesV2 { 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) - } + assert(quote.outAmount > 0.0, message: "syWFLOWvStrategy deposit: collateral→MOET quote returned zero — check pool liquidity or path config") + let moetVault <- preSwapper.swap(quote: quote, inVault: <-incoming) + assert(moetVault.balance > 0.0, message: "syWFLOWvStrategy deposit: collateral→MOET swap produced zero output") + self.sink.depositCapacity(from: &moetVault as auth(FungibleToken.Withdraw) &{FungibleToken.Vault}) + Burner.burn(<-moetVault) } else { Burner.burn(<-incoming) } @@ -774,24 +772,15 @@ 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 = self._buildMoetToCollateralSwapper(uniqueID: 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) + let id = self.uniqueID! + let moetToOrigSwapper = self._buildMoetToCollateralSwapper(uniqueID: id) + ?? panic("closePosition: no MOET→collateral swapper for \(collateralType.identifier)") + assert(collateralVault.balance > 0.0, message: "closePosition: zero-debt MOET vault has zero balance") + let quote = moetToOrigSwapper.quoteOut(forProvided: collateralVault.balance, reverse: false) + assert(quote.outAmount > 0.0, message: "closePosition: MOET→\(collateralType.identifier) quote returned zero") + let extVault <- moetToOrigSwapper.swap(quote: quote, inVault: <-collateralVault) self.positionClosed = true - return <- DeFiActionsUtils.getEmptyVault(collateralType) + return <- extVault } self.positionClosed = true return <- collateralVault @@ -858,16 +847,12 @@ access(all) contract FlowYieldVaultsStrategiesV2 { let quote = flowToCollateral.quoteIn(forDesired: buffered, reverse: true) if quote.inAmount > 0.0 { let extraCollateral <- self.source.withdrawAvailable(maxAmount: quote.inAmount) - if extraCollateral.balance > 0.0 { - let extraFlow <- flowToCollateral.swapBack(quote: quote, residual: <-extraCollateral) - if extraFlow.balance > 0.0 { - self.position.deposit(from: <-extraFlow) - } else { - Burner.burn(<-extraFlow) - } - } else { - Burner.burn(<-extraCollateral) - } + assert(extraCollateral.balance > 0.0, + message: "Pre-supplement: no collateral available to cover shortfall of \(shortfall) FLOW") + let extraFlow <- flowToCollateral.swapBack(quote: quote, residual: <-extraCollateral) + assert(extraFlow.balance > 0.0, + message: "Pre-supplement: collateral→FLOW swap produced zero output") + self.position.deposit(from: <-extraFlow) } } @@ -900,21 +885,15 @@ access(all) contract FlowYieldVaultsStrategiesV2 { // Convert internal collateral (MOET) → external collateral (e.g. PYUSD0) if needed if internalCollateralType != collateralType { - if let id = self.uniqueID { - if let moetToOrigSwapper = self._buildMoetToCollateralSwapper(uniqueID: 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) + let id = self.uniqueID! + let moetToOrigSwapper = self._buildMoetToCollateralSwapper(uniqueID: id) + ?? panic("closePosition: no MOET→collateral swapper for \(collateralType.identifier)") + assert(collateralVault.balance > 0.0, message: "closePosition: MOET vault has zero balance after close") + let quote = moetToOrigSwapper.quoteOut(forProvided: collateralVault.balance, reverse: false) + assert(quote.outAmount > 0.0, message: "closePosition: MOET→\(collateralType.identifier) quote returned zero") + let extVault <- moetToOrigSwapper.swap(quote: quote, inVault: <-collateralVault) self.positionClosed = true - return <- DeFiActionsUtils.getEmptyVault(collateralType) + return <- extVault } self.positionClosed = true @@ -1036,10 +1015,18 @@ access(all) contract FlowYieldVaultsStrategiesV2 { composer: Type<@MoreERC4626StrategyComposer>(), collateral: origType ) { + // Extend the FLOW→collateral path with collateralToMoet[1..] (skip collateral address, + // already the last element of debtToCollateral) and all collateralToMoet fees. + // e.g. debtToCollateral=[WFLOW,PYUSD0], collateralToMoet=[PYUSD0,MOET] + // → [WFLOW, PYUSD0, MOET], fees=[..., 100] var path = closeConfig.debtToCollateralUniV3AddressPath - path.append(preswapCfg.collateralToMoetAddressPath[preswapCfg.collateralToMoetAddressPath.length - 1]) + for i in InclusiveRange(1, preswapCfg.collateralToMoetAddressPath.length - 1) { + path.append(preswapCfg.collateralToMoetAddressPath[i]) + } var fees = closeConfig.debtToCollateralUniV3FeePath - fees.append(preswapCfg.collateralToMoetFeePath[preswapCfg.collateralToMoetFeePath.length - 1]) + for fee in preswapCfg.collateralToMoetFeePath { + fees.append(fee) + } return FlowYieldVaultsStrategiesV2._buildUniV3Swapper( tokenPath: path, feePath: fees, @@ -1264,6 +1251,7 @@ access(all) contract FlowYieldVaultsStrategiesV2 { ): @{FlowYieldVaults.Strategy} { pre { self.config[type] != nil: "Unsupported strategy type \(type.identifier)" + self.config[type]!.length > 0: "No collateral configured for strategy type \(type.identifier)" } let collateralType = withFunds.getType() @@ -1742,7 +1730,9 @@ access(all) contract FlowYieldVaultsStrategiesV2 { uniqueID: uniqueID ) let preSwapQuote = preSwapper.quoteOut(forProvided: withFunds.balance, reverse: false) + assert(preSwapQuote.outAmount > 0.0, message: "Pre-swap: collateral→MOET quote returned zero — check pool liquidity or path config") let moetFunds <- preSwapper.swap(quote: preSwapQuote, inVault: <-withFunds) + assert(moetFunds.balance > 0.0, message: "Pre-swap: collateral→MOET swap produced zero MOET output") // Open FlowALP position with MOET as collateral let positionPreswap <- FlowYieldVaultsStrategiesV2._openCreditPosition( @@ -1875,6 +1865,8 @@ access(all) contract FlowYieldVaultsStrategiesV2 { "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" + debtToCollateralAddressPath[0].equals(yieldToUnderlyingAddressPath[yieldToUnderlyingAddressPath.length - 1]): + "debtToCollateral UniV3 path must start with the underlying asset (end of yieldToUnderlying path)" } return MoreERC4626CollateralConfig( yieldTokenEVMAddress: yieldTokenEVMAddress, @@ -1949,9 +1941,9 @@ access(all) contract FlowYieldVaultsStrategiesV2 { self.configs[type] ?? panic("No config registered for \(type.identifier)") ) case Type<@MoreERC4626StrategyComposer>(): - return <- create MoreERC4626StrategyComposer( - FlowYieldVaultsStrategiesV2._getMoreERC4626ComposerConfig(type) - ) + let moreCfg = FlowYieldVaultsStrategiesV2._getMoreERC4626ComposerConfig(type) + assert(moreCfg.length > 0, message: "No config registered for \(type.identifier)") + return <- create MoreERC4626StrategyComposer(moreCfg) default: panic("Unsupported StrategyComposer \(type.identifier) requested") } From b79183ffdb3459057d26ac368a153d9f25ebc1da Mon Sep 17 00:00:00 2001 From: Alex <12097569+nialexsan@users.noreply.github.com> Date: Mon, 23 Mar 2026 12:14:56 -0400 Subject: [PATCH 41/72] address PR comments --- .../contracts/FlowYieldVaultsStrategiesV2.cdc | 87 +++++++++++++------ 1 file changed, 61 insertions(+), 26 deletions(-) diff --git a/cadence/contracts/FlowYieldVaultsStrategiesV2.cdc b/cadence/contracts/FlowYieldVaultsStrategiesV2.cdc index 9b5eef35..951f0c4f 100644 --- a/cadence/contracts/FlowYieldVaultsStrategiesV2.cdc +++ b/cadence/contracts/FlowYieldVaultsStrategiesV2.cdc @@ -55,6 +55,19 @@ access(all) contract FlowYieldVaultsStrategiesV2 { /// Canonical StoragePath where the StrategyComposerIssuer should be stored access(all) let IssuerStoragePath: StoragePath + /// Emitted when a non-empty vault is destroyed because the swapper quote returned zero output, + /// indicating the balance is too small to route (dust). Includes the quote as evidence of why + /// the burn decision was made, to aid debugging of stale or misconfigured swapper paths. + access(all) event DustBurned( + tokenType: String, + balance: UFix64, + quoteInType: String, + quoteOutType: String, + quoteInAmount: UFix64, + quoteOutAmount: UFix64, + swapperType: String + ) + /// A Source that converts yield tokens to debt tokens by pulling ALL available yield /// tokens from the wrapped source, rather than using quoteIn to limit the pull amount. /// @@ -376,18 +389,19 @@ access(all) contract FlowYieldVaultsStrategiesV2 { uniqueID: self.uniqueID! ) let shortfall = totalDebtAmount - expectedMOET - // Add 1% buffer to account for swap slippage/rounding in the collateral→MOET leg + // Over-deposit by 1% so the remaining debt lands below expectedMOET, giving + // BufferedSwapSource enough margin to cover ERC4626 floor-rounding at redemption let buffered = shortfall + shortfall / 100.0 let quote = collateralToMoetSwapper.quoteIn(forDesired: buffered, reverse: false) - if quote.inAmount > 0.0 { - let extraCollateral <- self.source.withdrawAvailable(maxAmount: quote.inAmount) - assert(extraCollateral.balance > 0.0, - message: "Pre-supplement: no collateral available to cover shortfall of \(shortfall) MOET") - let extraMOET <- collateralToMoetSwapper.swap(quote: quote, inVault: <-extraCollateral) - assert(extraMOET.balance > 0.0, - message: "Pre-supplement: collateral→MOET swap produced zero output") - self.position.deposit(from: <-extraMOET) - } + assert(quote.inAmount > 0.0, + message: "Pre-supplement: collateral→MOET quote returned zero input for non-zero shortfall — swapper misconfigured") + let extraCollateral <- self.source.withdrawAvailable(maxAmount: quote.inAmount) + assert(extraCollateral.balance > 0.0, + message: "Pre-supplement: no collateral available to cover shortfall of \(shortfall) MOET") + let extraMOET <- collateralToMoetSwapper.swap(quote: quote, inVault: <-extraCollateral) + assert(extraMOET.balance > 0.0, + message: "Pre-supplement: collateral→MOET swap produced zero output") + self.position.deposit(from: <-extraMOET) } // Step 7: Create a BufferedSwapSource that converts ALL yield tokens → MOET. @@ -420,17 +434,27 @@ access(all) contract FlowYieldVaultsStrategiesV2 { let v <- resultVaults.removeFirst() if v.getType() == collateralType { collateralVault.deposit(from: <-v) - } else if v.balance > 0.0 { + } else if v.balance == 0 { + // destroy empty vault + Burner.burn(<-v) + } else { // Quote first — if dust is too small to route, destroy it let quote = debtToCollateralSwapper.quoteOut(forProvided: v.balance, reverse: false) if quote.outAmount > 0.0 { let swapped <- debtToCollateralSwapper.swap(quote: quote, inVault: <-v) collateralVault.deposit(from: <-swapped) } else { + emit DustBurned( + tokenType: v.getType().identifier, + balance: v.balance, + quoteInType: quote.inType.identifier, + quoteOutType: quote.outType.identifier, + quoteInAmount: quote.inAmount, + quoteOutAmount: quote.outAmount, + swapperType: debtToCollateralSwapper.getType().identifier + ) Burner.burn(<-v) } - } else { - Burner.burn(<-v) } } @@ -842,18 +866,19 @@ access(all) contract FlowYieldVaultsStrategiesV2 { let expectedFlow = flowSource.minimumAvailable() if expectedFlow < totalDebtAmount { let shortfall = totalDebtAmount - expectedFlow - // Add 1% buffer to account for swap slippage/rounding in the collateral→FLOW leg + // Over-deposit by 1% so the remaining debt lands below expectedFlow, giving + // BufferedSwapSource enough margin to cover ERC4626 floor-rounding at redemption let buffered = shortfall + shortfall / 100.0 let quote = flowToCollateral.quoteIn(forDesired: buffered, reverse: true) - if quote.inAmount > 0.0 { - let extraCollateral <- self.source.withdrawAvailable(maxAmount: quote.inAmount) - assert(extraCollateral.balance > 0.0, - message: "Pre-supplement: no collateral available to cover shortfall of \(shortfall) FLOW") - let extraFlow <- flowToCollateral.swapBack(quote: quote, residual: <-extraCollateral) - assert(extraFlow.balance > 0.0, - message: "Pre-supplement: collateral→FLOW swap produced zero output") - self.position.deposit(from: <-extraFlow) - } + assert(quote.inAmount > 0.0, + message: "Pre-supplement: collateral→FLOW quote returned zero input for non-zero shortfall — swapper misconfigured") + let extraCollateral <- self.source.withdrawAvailable(maxAmount: quote.inAmount) + assert(extraCollateral.balance > 0.0, + message: "Pre-supplement: no collateral available to cover shortfall of \(shortfall) FLOW") + let extraFlow <- flowToCollateral.swapBack(quote: quote, residual: <-extraCollateral) + assert(extraFlow.balance > 0.0, + message: "Pre-supplement: collateral→FLOW swap produced zero output") + self.position.deposit(from: <-extraFlow) } // Step 7: Close position — pool pulls the (now pre-reduced) FLOW debt from flowSource @@ -867,17 +892,27 @@ access(all) contract FlowYieldVaultsStrategiesV2 { let v <- resultVaults.removeFirst() if v.getType() == internalCollateralType { collateralVault.deposit(from: <-v) - } else if v.balance > 0.0 { + } else if v.balance == 0.0 { + // destroy empty vault + Burner.burn(<-v) + } else { // FLOW overpayment dust — convert back to collateral if routable let quote = flowToCollateral.quoteOut(forProvided: v.balance, reverse: false) if quote.outAmount > 0.0 { let swapped <- flowToCollateral.swap(quote: quote, inVault: <-v) collateralVault.deposit(from: <-swapped) } else { + emit DustBurned( + tokenType: v.getType().identifier, + balance: v.balance, + quoteInType: quote.inType.identifier, + quoteOutType: quote.outType.identifier, + quoteInAmount: quote.inAmount, + quoteOutAmount: quote.outAmount, + swapperType: flowToCollateral.getType().identifier + ) Burner.burn(<-v) } - } else { - Burner.burn(<-v) } } From 67ae9ff3aac329f1564fa96e99e0d0c130166fa4 Mon Sep 17 00:00:00 2001 From: Alex <12097569+nialexsan@users.noreply.github.com> Date: Wed, 25 Mar 2026 21:52:42 -0400 Subject: [PATCH 42/72] deploy missing contracts --- ...lowYieldVaultsStrategiesV2_FUSDEV_test.cdc | 24 +++++++++++++++++++ ...wYieldVaultsStrategiesV2_syWFLOWv_test.cdc | 24 +++++++++++++++++++ 2 files changed, 48 insertions(+) diff --git a/cadence/tests/FlowYieldVaultsStrategiesV2_FUSDEV_test.cdc b/cadence/tests/FlowYieldVaultsStrategiesV2_FUSDEV_test.cdc index e6d46eea..380a52bf 100644 --- a/cadence/tests/FlowYieldVaultsStrategiesV2_FUSDEV_test.cdc +++ b/cadence/tests/FlowYieldVaultsStrategiesV2_FUSDEV_test.cdc @@ -193,6 +193,30 @@ access(all) fun setup() { ) Test.expect(err, Test.beNil()) + log("Deploying UInt64LinkedList...") + err = Test.deployContract( + name: "UInt64LinkedList", + path: "../../cadence/contracts/UInt64LinkedList.cdc", + arguments: [] + ) + Test.expect(err, Test.beNil()) + + log("Deploying AutoBalancers...") + err = Test.deployContract( + name: "AutoBalancers", + path: "../../cadence/contracts/AutoBalancers.cdc", + arguments: [] + ) + Test.expect(err, Test.beNil()) + + log("Deploying FlowYieldVaultsSchedulerRegistry...") + err = Test.deployContract( + name: "FlowYieldVaultsSchedulerRegistry", + path: "../../cadence/contracts/FlowYieldVaultsSchedulerRegistry.cdc", + arguments: [] + ) + Test.expect(err, Test.beNil()) + log("Deploying FlowYieldVaultsAutoBalancers...") err = Test.deployContract( name: "FlowYieldVaultsAutoBalancers", diff --git a/cadence/tests/FlowYieldVaultsStrategiesV2_syWFLOWv_test.cdc b/cadence/tests/FlowYieldVaultsStrategiesV2_syWFLOWv_test.cdc index e4910ef3..30a07f4c 100644 --- a/cadence/tests/FlowYieldVaultsStrategiesV2_syWFLOWv_test.cdc +++ b/cadence/tests/FlowYieldVaultsStrategiesV2_syWFLOWv_test.cdc @@ -187,6 +187,30 @@ access(all) fun setup() { ) Test.expect(err, Test.beNil()) + log("Deploying UInt64LinkedList...") + err = Test.deployContract( + name: "UInt64LinkedList", + path: "../../cadence/contracts/UInt64LinkedList.cdc", + arguments: [] + ) + Test.expect(err, Test.beNil()) + + log("Deploying AutoBalancers...") + err = Test.deployContract( + name: "AutoBalancers", + path: "../../cadence/contracts/AutoBalancers.cdc", + arguments: [] + ) + Test.expect(err, Test.beNil()) + + log("Deploying FlowYieldVaultsSchedulerRegistry...") + err = Test.deployContract( + name: "FlowYieldVaultsSchedulerRegistry", + path: "../../cadence/contracts/FlowYieldVaultsSchedulerRegistry.cdc", + arguments: [] + ) + Test.expect(err, Test.beNil()) + log("Deploying FlowYieldVaultsAutoBalancers...") err = Test.deployContract( name: "FlowYieldVaultsAutoBalancers", From 061e7280e0f7200a3985153bba1b51d64002cd19 Mon Sep 17 00:00:00 2001 From: Alex <12097569+nialexsan@users.noreply.github.com> Date: Thu, 26 Mar 2026 12:03:50 -0400 Subject: [PATCH 43/72] fix typo --- cadence/contracts/FlowYieldVaultsStrategiesV2.cdc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cadence/contracts/FlowYieldVaultsStrategiesV2.cdc b/cadence/contracts/FlowYieldVaultsStrategiesV2.cdc index 06492e66..0b3dd058 100644 --- a/cadence/contracts/FlowYieldVaultsStrategiesV2.cdc +++ b/cadence/contracts/FlowYieldVaultsStrategiesV2.cdc @@ -435,7 +435,7 @@ access(all) contract FlowYieldVaultsStrategiesV2 { let v <- resultVaults.removeFirst() if v.getType() == collateralType { collateralVault.deposit(from: <-v) - } else if v.balance == 0 { + } else if v.balance == 0.0 { // destroy empty vault Burner.burn(<-v) } else { From 73f991ebde6b1f1eae9779fa8e595783368d03cc Mon Sep 17 00:00:00 2001 From: Alex <12097569+nialexsan@users.noreply.github.com> Date: Thu, 26 Mar 2026 12:38:03 -0400 Subject: [PATCH 44/72] fix v2 autobalancer struct --- .../contracts/FlowYieldVaultsStrategiesV2.cdc | 35 +++++++++++++------ ...lowYieldVaultsStrategiesV2_FUSDEV_test.cdc | 12 +++---- ...wYieldVaultsStrategiesV2_syWFLOWv_test.cdc | 12 +++---- 3 files changed, 37 insertions(+), 22 deletions(-) diff --git a/cadence/contracts/FlowYieldVaultsStrategiesV2.cdc b/cadence/contracts/FlowYieldVaultsStrategiesV2.cdc index 0b3dd058..a6cb2747 100644 --- a/cadence/contracts/FlowYieldVaultsStrategiesV2.cdc +++ b/cadence/contracts/FlowYieldVaultsStrategiesV2.cdc @@ -390,10 +390,7 @@ access(all) contract FlowYieldVaultsStrategiesV2 { uniqueID: self.uniqueID! ) let shortfall = totalDebtAmount - expectedMOET - // Over-deposit by 1% so the remaining debt lands below expectedMOET, giving - // BufferedSwapSource enough margin to cover ERC4626 floor-rounding at redemption - let buffered = shortfall + shortfall / 100.0 - let quote = collateralToMoetSwapper.quoteIn(forDesired: buffered, reverse: false) + let quote = collateralToMoetSwapper.quoteIn(forDesired: shortfall, reverse: false) assert(quote.inAmount > 0.0, message: "Pre-supplement: collateral→MOET quote returned zero input for non-zero shortfall — swapper misconfigured") let extraCollateral <- self.source.withdrawAvailable(maxAmount: quote.inAmount) @@ -867,10 +864,7 @@ access(all) contract FlowYieldVaultsStrategiesV2 { let expectedFlow = flowSource.minimumAvailable() if expectedFlow < totalDebtAmount { let shortfall = totalDebtAmount - expectedFlow - // Over-deposit by 1% so the remaining debt lands below expectedFlow, giving - // BufferedSwapSource enough margin to cover ERC4626 floor-rounding at redemption - let buffered = shortfall + shortfall / 100.0 - let quote = flowToCollateral.quoteIn(forDesired: buffered, reverse: true) + let quote = flowToCollateral.quoteIn(forDesired: shortfall, reverse: true) assert(quote.inAmount > 0.0, message: "Pre-supplement: collateral→FLOW quote returned zero input for non-zero shortfall — swapper misconfigured") let extraCollateral <- self.source.withdrawAvailable(maxAmount: quote.inAmount) @@ -1111,8 +1105,29 @@ access(all) contract FlowYieldVaultsStrategiesV2 { } } + // @deprecated /// Returned bundle for stored AutoBalancer interactions (reference + caps) access(all) struct AutoBalancerIO { + access(all) let autoBalancer: + auth(DeFiActions.Auto, DeFiActions.Set, DeFiActions.Get, DeFiActions.Schedule, FungibleToken.Withdraw) + &DeFiActions.AutoBalancer + + access(all) let sink: {DeFiActions.Sink} + access(all) let source: {DeFiActions.Source} + + init( + autoBalancer: auth(DeFiActions.Auto, DeFiActions.Set, DeFiActions.Get, DeFiActions.Schedule, FungibleToken.Withdraw) &DeFiActions.AutoBalancer, + sink: {DeFiActions.Sink}, + source: {DeFiActions.Source} + ) { + self.sink = sink + self.source = source + self.autoBalancer = autoBalancer + } + } + + /// Returned bundle for stored AutoBalancer interactions (reference + caps) + access(all) struct AutoBalancerIO_v2 { access(all) let autoBalancer: auth(AutoBalancers.Auto, AutoBalancers.Set, AutoBalancers.Get, AutoBalancers.Schedule, FungibleToken.Withdraw) &AutoBalancers.AutoBalancer @@ -1187,7 +1202,7 @@ access(all) contract FlowYieldVaultsStrategiesV2 { yieldTokenType: Type, recurringConfig: AutoBalancers.AutoBalancerRecurringConfig?, uniqueID: DeFiActions.UniqueIdentifier - ): FlowYieldVaultsStrategiesV2.AutoBalancerIO { + ): FlowYieldVaultsStrategiesV2.AutoBalancerIO_v2 { let autoBalancerRef = FlowYieldVaultsAutoBalancersV1._initNewAutoBalancer( oracle: oracle, @@ -1205,7 +1220,7 @@ access(all) contract FlowYieldVaultsStrategiesV2 { let source = autoBalancerRef.createBalancerSource() ?? panic("Could not retrieve Source from AutoBalancer with id \(uniqueID.id)") - return FlowYieldVaultsStrategiesV2.AutoBalancerIO( + return FlowYieldVaultsStrategiesV2.AutoBalancerIO_v2( autoBalancer: autoBalancerRef, sink: sink, source: source diff --git a/cadence/tests/FlowYieldVaultsStrategiesV2_FUSDEV_test.cdc b/cadence/tests/FlowYieldVaultsStrategiesV2_FUSDEV_test.cdc index 380a52bf..ae4573fb 100644 --- a/cadence/tests/FlowYieldVaultsStrategiesV2_FUSDEV_test.cdc +++ b/cadence/tests/FlowYieldVaultsStrategiesV2_FUSDEV_test.cdc @@ -209,18 +209,18 @@ access(all) fun setup() { ) Test.expect(err, Test.beNil()) - log("Deploying FlowYieldVaultsSchedulerRegistry...") + log("Deploying FlowYieldVaultsSchedulerRegistryV1...") err = Test.deployContract( - name: "FlowYieldVaultsSchedulerRegistry", - path: "../../cadence/contracts/FlowYieldVaultsSchedulerRegistry.cdc", + name: "FlowYieldVaultsSchedulerRegistryV1", + path: "../../cadence/contracts/FlowYieldVaultsSchedulerRegistryV1.cdc", arguments: [] ) Test.expect(err, Test.beNil()) - log("Deploying FlowYieldVaultsAutoBalancers...") + log("Deploying FlowYieldVaultsAutoBalancersV1...") err = Test.deployContract( - name: "FlowYieldVaultsAutoBalancers", - path: "../../cadence/contracts/FlowYieldVaultsAutoBalancers.cdc", + name: "FlowYieldVaultsAutoBalancersV1", + path: "../../cadence/contracts/FlowYieldVaultsAutoBalancersV1.cdc", arguments: [] ) Test.expect(err, Test.beNil()) diff --git a/cadence/tests/FlowYieldVaultsStrategiesV2_syWFLOWv_test.cdc b/cadence/tests/FlowYieldVaultsStrategiesV2_syWFLOWv_test.cdc index 30a07f4c..ad0d0eec 100644 --- a/cadence/tests/FlowYieldVaultsStrategiesV2_syWFLOWv_test.cdc +++ b/cadence/tests/FlowYieldVaultsStrategiesV2_syWFLOWv_test.cdc @@ -203,18 +203,18 @@ access(all) fun setup() { ) Test.expect(err, Test.beNil()) - log("Deploying FlowYieldVaultsSchedulerRegistry...") + log("Deploying FlowYieldVaultsSchedulerRegistryV1...") err = Test.deployContract( - name: "FlowYieldVaultsSchedulerRegistry", - path: "../../cadence/contracts/FlowYieldVaultsSchedulerRegistry.cdc", + name: "FlowYieldVaultsSchedulerRegistryV1", + path: "../../cadence/contracts/FlowYieldVaultsSchedulerRegistryV1.cdc", arguments: [] ) Test.expect(err, Test.beNil()) - log("Deploying FlowYieldVaultsAutoBalancers...") + log("Deploying FlowYieldVaultsAutoBalancersV1...") err = Test.deployContract( - name: "FlowYieldVaultsAutoBalancers", - path: "../../cadence/contracts/FlowYieldVaultsAutoBalancers.cdc", + name: "FlowYieldVaultsAutoBalancersV1", + path: "../../cadence/contracts/FlowYieldVaultsAutoBalancersV1.cdc", arguments: [] ) Test.expect(err, Test.beNil()) From 1132f00999f5df5f5aafd2298ac0905b50d7a857 Mon Sep 17 00:00:00 2001 From: Alex <12097569+nialexsan@users.noreply.github.com> Date: Thu, 26 Mar 2026 12:43:29 -0400 Subject: [PATCH 45/72] tweak assertion --- cadence/contracts/FlowYieldVaultsStrategiesV2.cdc | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/cadence/contracts/FlowYieldVaultsStrategiesV2.cdc b/cadence/contracts/FlowYieldVaultsStrategiesV2.cdc index a6cb2747..f62dddb1 100644 --- a/cadence/contracts/FlowYieldVaultsStrategiesV2.cdc +++ b/cadence/contracts/FlowYieldVaultsStrategiesV2.cdc @@ -397,8 +397,8 @@ access(all) contract FlowYieldVaultsStrategiesV2 { assert(extraCollateral.balance > 0.0, message: "Pre-supplement: no collateral available to cover shortfall of \(shortfall) MOET") let extraMOET <- collateralToMoetSwapper.swap(quote: quote, inVault: <-extraCollateral) - assert(extraMOET.balance > 0.0, - message: "Pre-supplement: collateral→MOET swap produced zero output") + assert(extraMOET.balance >= shortfall, + message: "Pre-supplement: collateral→MOET swap produced less than shortfall: got \(extraMOET.balance), need \(shortfall)") self.position.deposit(from: <-extraMOET) } @@ -871,8 +871,8 @@ access(all) contract FlowYieldVaultsStrategiesV2 { assert(extraCollateral.balance > 0.0, message: "Pre-supplement: no collateral available to cover shortfall of \(shortfall) FLOW") let extraFlow <- flowToCollateral.swapBack(quote: quote, residual: <-extraCollateral) - assert(extraFlow.balance > 0.0, - message: "Pre-supplement: collateral→FLOW swap produced zero output") + assert(extraFlow.balance >= shortfall, + message: "Pre-supplement: collateral→FLOW swap produced less than shortfall: got \(extraFlow.balance), need \(shortfall)") self.position.deposit(from: <-extraFlow) } From 5ece4d951936e271a2983baab10fc339e3062daf Mon Sep 17 00:00:00 2001 From: Alex <12097569+nialexsan@users.noreply.github.com> Date: Thu, 26 Mar 2026 15:28:17 -0400 Subject: [PATCH 46/72] allow PYUSD0 as collateral, leave deposit and close position reverse preswap logic for legacy positions --- .../contracts/FlowYieldVaultsStrategiesV2.cdc | 64 +------------------ ...wYieldVaultsStrategiesV2_syWFLOWv_test.cdc | 37 +++-------- 2 files changed, 10 insertions(+), 91 deletions(-) diff --git a/cadence/contracts/FlowYieldVaultsStrategiesV2.cdc b/cadence/contracts/FlowYieldVaultsStrategiesV2.cdc index f62dddb1..312d3143 100644 --- a/cadence/contracts/FlowYieldVaultsStrategiesV2.cdc +++ b/cadence/contracts/FlowYieldVaultsStrategiesV2.cdc @@ -49,6 +49,8 @@ access(all) contract FlowYieldVaultsStrategiesV2 { /// "closedPositions" → {UInt64: Bool} /// "syWFLOWvDebtTokenTypes" → {UInt64: Type} /// "moreERC4626Configs" → {Type: {Type: {Type: MoreERC4626CollateralConfig}}} + + // @deprecated - should be used only for working with legacy positions /// "moetPreswapConfigs" → {Type: {Type: MoetPreswapConfig}} /// "originalCollateralTypes" → {UInt64: Type} access(contract) let config: {String: AnyStruct} @@ -1764,68 +1766,6 @@ 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) - assert(preSwapQuote.outAmount > 0.0, message: "Pre-swap: collateral→MOET quote returned zero — check pool liquidity or path config") - let moetFunds <- preSwapper.swap(quote: preSwapQuote, inVault: <-withFunds) - assert(moetFunds.balance > 0.0, message: "Pre-swap: collateral→MOET swap produced zero MOET output") - - // 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) - - FlowYieldVaultsStrategiesV2._setSyWFLOWvDebtTokenType(uniqueID.id, flowDebtTokenType) - FlowYieldVaultsStrategiesV2._setOriginalCollateralType(uniqueID.id, collateralType) - - return <-create syWFLOWvStrategy( - id: uniqueID, - collateralType: tokens.moetTokenType, - position: <-positionPreswap - ) - } - - // --- Standard path (WBTC, WETH — directly supported by FlowALP) --- - // Open FlowALP position with collateral; drawDownSink accepts FLOW let positionFlow <- FlowYieldVaultsStrategiesV2._openCreditPosition( funds: <-withFunds, diff --git a/cadence/tests/FlowYieldVaultsStrategiesV2_syWFLOWv_test.cdc b/cadence/tests/FlowYieldVaultsStrategiesV2_syWFLOWv_test.cdc index ad0d0eec..e6eda72e 100644 --- a/cadence/tests/FlowYieldVaultsStrategiesV2_syWFLOWv_test.cdc +++ b/cadence/tests/FlowYieldVaultsStrategiesV2_syWFLOWv_test.cdc @@ -347,27 +347,6 @@ 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...") @@ -421,9 +400,9 @@ access(all) fun testCreateSyWFLOWvYieldVault_PYUSD0() { 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) + // Verify the PYUSD0→MOET pre-swap DID NOT happen by checking FlowALPv0.Deposited events: + // - There must be NO Deposited event with vaultType = MOET (pre-swapped collateral) + // - There must be a Deposited event with vaultType = PYUSD0 (should never reach FlowALP) let depositedEvents = Test.eventsOfType(Type()) log("FlowALPv0.Deposited events: ".concat(depositedEvents.length.toString())) @@ -440,11 +419,11 @@ access(all) fun testCreateSyWFLOWvYieldVault_PYUSD0() { 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)") + Test.assert(!foundMoetDeposit, + message: "Unxpected FlowALPv0.Deposited event with MOET — pre-swap did not deposit PYUSD0 into FlowALP") + Test.assert(foundPyusd0Deposit, + message: "Expected FlowALPv0.Deposited event with PYUSD0 — pre-swap was bypassed") + log("Confirmed: FlowALP received PYUSD0 as collateral (MOET was NOT pre-swapped before FlowALP deposit)") } access(all) fun testDepositToSyWFLOWvYieldVault_PYUSD0() { From 8461f2ba1e11a9ea6afccd5bf21b07eb81b005ad Mon Sep 17 00:00:00 2001 From: Alex <12097569+nialexsan@users.noreply.github.com> Date: Fri, 27 Mar 2026 11:21:54 -0400 Subject: [PATCH 47/72] deprecate swap source --- .../contracts/FlowYieldVaultsStrategiesV2.cdc | 68 ++++--------------- 1 file changed, 14 insertions(+), 54 deletions(-) diff --git a/cadence/contracts/FlowYieldVaultsStrategiesV2.cdc b/cadence/contracts/FlowYieldVaultsStrategiesV2.cdc index 312d3143..a7919950 100644 --- a/cadence/contracts/FlowYieldVaultsStrategiesV2.cdc +++ b/cadence/contracts/FlowYieldVaultsStrategiesV2.cdc @@ -71,16 +71,8 @@ access(all) contract FlowYieldVaultsStrategiesV2 { swapperType: String ) - /// A Source that converts yield tokens to debt tokens by pulling ALL available yield - /// tokens from the wrapped source, rather than using quoteIn to limit the pull amount. - /// - /// This avoids ERC4626 rounding issues where quoteIn might underestimate required shares, - /// causing the swap to return less than the requested debt amount. By pulling everything - /// and swapping everything, the output is as large as the yield position allows. - /// - /// The caller is responsible for ensuring the yield tokens (after swapping) will cover the - /// required debt — e.g. by pre-depositing supplemental MOET to reduce the position's debt - /// before calling closePosition (see FUSDEVStrategy.closePosition step 6). + /// Deprecated — replaced by SwapConnectors.SwapSource. Kept as a no-op to preserve + /// contract upgrade compatibility (Cadence structs cannot be removed once deployed). access(all) struct BufferedSwapSource : DeFiActions.Source { access(self) let swapper: {DeFiActions.Swapper} access(self) let source: {DeFiActions.Source} @@ -91,52 +83,20 @@ access(all) contract FlowYieldVaultsStrategiesV2 { source: {DeFiActions.Source}, uniqueID: DeFiActions.UniqueIdentifier? ) { - pre { - source.getSourceType() == swapper.inType(): - "source type != swapper inType" - } self.swapper = swapper self.source = source self.uniqueID = uniqueID } access(all) fun getComponentInfo(): DeFiActions.ComponentInfo { - return DeFiActions.ComponentInfo( - type: self.getType(), - id: self.id(), - innerComponents: [ - self.swapper.getComponentInfo(), - self.source.getComponentInfo() - ] - ) + return DeFiActions.ComponentInfo(type: self.getType(), id: self.id(), innerComponents: []) } access(contract) view fun copyID(): DeFiActions.UniqueIdentifier? { return self.uniqueID } access(contract) fun setID(_ id: DeFiActions.UniqueIdentifier?) { self.uniqueID = id } access(all) view fun getSourceType(): Type { return self.swapper.outType() } - access(all) fun minimumAvailable(): UFix64 { - let avail = self.source.minimumAvailable() - if avail == 0.0 { return 0.0 } - return self.swapper.quoteOut(forProvided: avail, reverse: false).outAmount - } - /// Pulls ALL available yield tokens from the source and swaps them to the debt token. - /// Ignores quoteIn — avoids ERC4626 rounding underestimates that would leave us short. + access(all) fun minimumAvailable(): UFix64 { return 0.0 } access(FungibleToken.Withdraw) fun withdrawAvailable(maxAmount: UFix64): @{FungibleToken.Vault} { - if maxAmount == 0.0 { - return <- DeFiActionsUtils.getEmptyVault(self.getSourceType()) - } - let availableIn = self.source.minimumAvailable() - if availableIn == 0.0 { - return <- DeFiActionsUtils.getEmptyVault(self.getSourceType()) - } - // Pull ALL available yield tokens (not quoteIn-limited) - let sourceLiquidity <- self.source.withdrawAvailable(maxAmount: availableIn) - if sourceLiquidity.balance == 0.0 { - Burner.burn(<-sourceLiquidity) - return <- DeFiActionsUtils.getEmptyVault(self.getSourceType()) - } - let swapped <- self.swapper.swap(quote: nil, inVault: <-sourceLiquidity) - assert(swapped.balance > 0.0, message: "BufferedSwapSource: swap returned zero despite available input") - return <- swapped + return <- DeFiActionsUtils.getEmptyVault(self.getSourceType()) } } @@ -404,10 +364,10 @@ access(all) contract FlowYieldVaultsStrategiesV2 { self.position.deposit(from: <-extraMOET) } - // Step 7: Create a BufferedSwapSource that converts ALL yield tokens → MOET. - // Pulling all (not quoteIn-limited) avoids ERC4626 rounding underestimates. - // After the pre-supplement above, the remaining debt is covered by the yield tokens. - let moetSource = FlowYieldVaultsStrategiesV2.BufferedSwapSource( + // Step 7: Create a SwapSource that converts yield tokens → MOET for debt repayment. + // Step 6's pre-supplement ensures remaining debt ≤ yield value, so SwapSource will + // use quoteIn(remainingDebt) and pull only the shares needed — not the full balance. + let moetSource = SwapConnectors.SwapSource( swapper: yieldToMoetSwapper, source: yieldTokenSource, uniqueID: self.copyID() @@ -839,11 +799,11 @@ access(all) contract FlowYieldVaultsStrategiesV2 { let yieldTokenSource = FlowYieldVaultsAutoBalancersV1.createExternalSource(id: self.id()!) ?? panic("Could not create external source from AutoBalancer") - // Step 5: Create a BufferedSwapSource that converts ALL syWFLOWv → FLOW for debt repayment. - // Pulls all available tokens (not quoteIn-limited) to avoid ERC4626 rounding underestimates - // that would leave us short of the required FLOW debt. Any FLOW overpayment is returned as - // dust and converted back to collateral below. - let flowSource = BufferedSwapSource( + // Step 5: Create a SwapSource that converts syWFLOWv → FLOW for debt repayment. + // SwapSource uses quoteIn when yield value >= debt (pulling only the needed shares), + // or quoteOut when yield is insufficient (pulling everything as a best-effort). + // Any FLOW overpayment is returned as dust and converted back to collateral below. + let flowSource = SwapConnectors.SwapSource( swapper: syWFLOWvToFlow, source: yieldTokenSource, uniqueID: self.copyID() From 900e37d462e3c3337aee86e705acb3ec66f2ac92 Mon Sep 17 00:00:00 2001 From: Alex <12097569+nialexsan@users.noreply.github.com> Date: Mon, 30 Mar 2026 16:02:14 -0400 Subject: [PATCH 48/72] don't burn excess yield --- .../contracts/FlowYieldVaultsStrategiesV2.cdc | 83 +++++++++++++++++++ 1 file changed, 83 insertions(+) diff --git a/cadence/contracts/FlowYieldVaultsStrategiesV2.cdc b/cadence/contracts/FlowYieldVaultsStrategiesV2.cdc index a7919950..8dd967ec 100644 --- a/cadence/contracts/FlowYieldVaultsStrategiesV2.cdc +++ b/cadence/contracts/FlowYieldVaultsStrategiesV2.cdc @@ -419,6 +419,48 @@ access(all) contract FlowYieldVaultsStrategiesV2 { } destroy resultVaults + + // Step 9: Drain any remaining FUSDEV shares from the AutoBalancer — excess yield + // not consumed during debt repayment — and convert them directly to collateral. + // The SwapSource inside closePosition only pulled what was needed to repay the debt; + // any surplus shares are still held by the AutoBalancer and are recovered here. + let excessShares <- yieldTokenSource.withdrawAvailable(maxAmount: UFix64.max) + if excessShares.balance > 0.0 { + let moetQuote = yieldToMoetSwapper.quoteOut(forProvided: excessShares.balance, reverse: false) + if moetQuote.outAmount > 0.0 { + let moetVault <- yieldToMoetSwapper.swap(quote: moetQuote, inVault: <-excessShares) + let collQuote = debtToCollateralSwapper.quoteOut(forProvided: moetVault.balance, reverse: false) + if collQuote.outAmount > 0.0 { + let extraCollateral <- debtToCollateralSwapper.swap(quote: collQuote, inVault: <-moetVault) + collateralVault.deposit(from: <-extraCollateral) + } else { + emit DustBurned( + tokenType: moetVault.getType().identifier, + balance: moetVault.balance, + quoteInType: collQuote.inType.identifier, + quoteOutType: collQuote.outType.identifier, + quoteInAmount: collQuote.inAmount, + quoteOutAmount: collQuote.outAmount, + swapperType: debtToCollateralSwapper.getType().identifier + ) + Burner.burn(<-moetVault) + } + } else { + emit DustBurned( + tokenType: excessShares.getType().identifier, + balance: excessShares.balance, + quoteInType: moetQuote.inType.identifier, + quoteOutType: moetQuote.outType.identifier, + quoteInAmount: moetQuote.inAmount, + quoteOutAmount: moetQuote.outAmount, + swapperType: yieldToMoetSwapper.getType().identifier + ) + Burner.burn(<-excessShares) + } + } else { + Burner.burn(<-excessShares) + } + self._markPositionClosed() return <- collateralVault } @@ -875,6 +917,47 @@ access(all) contract FlowYieldVaultsStrategiesV2 { destroy resultVaults + // Step 8: Drain any remaining syWFLOWv shares from the AutoBalancer — excess yield + // not consumed during debt repayment — and convert them directly to collateral. + // The SwapSource inside closePosition only pulled what was needed to repay the debt; + // any surplus shares are still held by the AutoBalancer and are recovered here. + let excessShares <- yieldTokenSource.withdrawAvailable(maxAmount: UFix64.max) + if excessShares.balance > 0.0 { + let flowQuote = syWFLOWvToFlow.quoteOut(forProvided: excessShares.balance, reverse: false) + if flowQuote.outAmount > 0.0 { + let flowVault <- syWFLOWvToFlow.swap(quote: flowQuote, inVault: <-excessShares) + let collQuote = flowToCollateral.quoteOut(forProvided: flowVault.balance, reverse: false) + if collQuote.outAmount > 0.0 { + let extraCollateral <- flowToCollateral.swap(quote: collQuote, inVault: <-flowVault) + collateralVault.deposit(from: <-extraCollateral) + } else { + emit DustBurned( + tokenType: flowVault.getType().identifier, + balance: flowVault.balance, + quoteInType: collQuote.inType.identifier, + quoteOutType: collQuote.outType.identifier, + quoteInAmount: collQuote.inAmount, + quoteOutAmount: collQuote.outAmount, + swapperType: flowToCollateral.getType().identifier + ) + Burner.burn(<-flowVault) + } + } else { + emit DustBurned( + tokenType: excessShares.getType().identifier, + balance: excessShares.balance, + quoteInType: flowQuote.inType.identifier, + quoteOutType: flowQuote.outType.identifier, + quoteInAmount: flowQuote.inAmount, + quoteOutAmount: flowQuote.outAmount, + swapperType: syWFLOWvToFlow.getType().identifier + ) + Burner.burn(<-excessShares) + } + } else { + Burner.burn(<-excessShares) + } + // Convert internal collateral (MOET) → external collateral (e.g. PYUSD0) if needed if internalCollateralType != collateralType { let id = self.uniqueID! From 31cd6d8e72edb609310322bfa2ee3c0ace37be92 Mon Sep 17 00:00:00 2001 From: Alex <12097569+nialexsan@users.noreply.github.com> Date: Mon, 30 Mar 2026 21:36:52 -0400 Subject: [PATCH 49/72] remove moet from strategies --- .../contracts/FlowYieldVaultsStrategiesV2.cdc | 517 +++++------------- ...lowYieldVaultsStrategiesV2_FUSDEV_test.cdc | 26 + 2 files changed, 164 insertions(+), 379 deletions(-) diff --git a/cadence/contracts/FlowYieldVaultsStrategiesV2.cdc b/cadence/contracts/FlowYieldVaultsStrategiesV2.cdc index 8dd967ec..35d1b0c7 100644 --- a/cadence/contracts/FlowYieldVaultsStrategiesV2.cdc +++ b/cadence/contracts/FlowYieldVaultsStrategiesV2.cdc @@ -20,8 +20,6 @@ import "FlowYieldVaults" import "FlowYieldVaultsAutoBalancersV1" // scheduler import "FlowTransactionScheduler" -// tokens -import "MOET" // vm bridge import "FlowEVMBridgeConfig" // live oracles @@ -200,9 +198,10 @@ access(all) contract FlowYieldVaultsStrategiesV2 { } /// This strategy uses FUSDEV vault (Morpho ERC4626). - /// Deposits collateral into a single FlowALP position, borrowing MOET as debt. - /// MOET is swapped to PYUSD0 and deposited into the Morpho FUSDEV ERC4626 vault. - /// Each strategy instance holds exactly one collateral type and one debt type (MOET). + /// Deposits collateral into a single FlowALP position, borrowing PYUSD0 as debt. + /// PYUSD0 is deposited directly into the Morpho FUSDEV ERC4626 vault (no AMM swap needed + /// since PYUSD0 is the vault's underlying asset). + /// Each strategy instance holds exactly one collateral type and one debt type (PYUSD0). /// PYUSD0 (the FUSDEV vault's underlying asset) cannot be used as collateral. access(all) resource FUSDEVStrategy : FlowYieldVaults.Strategy, DeFiActions.IdentifiableResource { /// An optional identifier allowing protocols to identify stacked connector operations by defining a protocol- @@ -255,8 +254,8 @@ access(all) contract FlowYieldVaultsStrategiesV2 { /// This method: /// 1. Calculates debt amount from position /// 2. Creates external yield token source from AutoBalancer - /// 3. Swaps yield tokens → MOET via stored swapper - /// 4. Closes position with prepared MOET vault + /// 3. Swaps yield tokens → PYUSD0 via stored swapper + /// 4. Closes position with prepared PYUSD0 vault /// /// This approach eliminates circular dependencies by preparing all funds externally /// before calling the position's close method. @@ -313,7 +312,7 @@ access(all) contract FlowYieldVaultsStrategiesV2 { let yieldTokenSource = FlowYieldVaultsAutoBalancersV1.createExternalSource(id: self.id()!) ?? panic("Could not create external source from AutoBalancer") - // Step 5: Reconstruct yield→MOET swapper from stored CollateralConfig. + // Step 5: Reconstruct yield→PYUSD0 swapper from stored CollateralConfig. let closeCollateralConfig = self._getStoredCollateralConfig( strategyType: Type<@FUSDEVStrategy>(), collateralType: collateralType @@ -321,67 +320,67 @@ access(all) contract FlowYieldVaultsStrategiesV2 { let closeTokens = FlowYieldVaultsStrategiesV2._resolveTokenBundle( yieldTokenEVMAddress: closeCollateralConfig.yieldTokenEVMAddress ) - let yieldToMoetSwapper = self._buildYieldToDebtSwapper( + let yieldToPyusd0Swapper = self._buildYieldToDebtSwapper( tokens: closeTokens, uniqueID: self.uniqueID! ) // Step 6: Pre-supplement from collateral if yield is insufficient to cover the full debt. // - // The FUSDEV close path has a structural ~0.02% round-trip fee loss: - // Open: MOET → PYUSD0 (UniV3 0.01%) → FUSDEV (ERC4626, free) - // Close: FUSDEV → PYUSD0 (ERC4626, free) → MOET (UniV3 0.01%) - // In production, accrued yield more than covers this; with no accrued yield (e.g. in - // tests, immediate open+close), the yield tokens convert back to slightly less MOET - // than was borrowed. We handle this by pre-pulling a tiny amount of collateral from - // self.source, swapping it to MOET, and depositing it into the position to reduce the - // outstanding debt — BEFORE calling position.closePosition. + // The FUSDEV close path has a negligible round-trip fee: + // Open: PYUSD0 → FUSDEV (ERC4626 deposit, free) + // Close: FUSDEV → PYUSD0 (ERC4626 redeem, free) + // In production, accrued yield more than covers any rounding; with no accrued yield + // (e.g. in tests, immediate open+close), the yield tokens may convert back to slightly + // less PYUSD0 than was borrowed. We handle this by pre-pulling a tiny amount of + // collateral from self.source, swapping it to PYUSD0, and depositing it into the + // position to reduce the outstanding debt — BEFORE calling position.closePosition. // // This MUST be done before closePosition because the position is locked during close: // any attempt to pull from self.source inside a repaymentSource.withdrawAvailable call // would trigger "Reentrancy: position X is locked". let yieldAvail = yieldTokenSource.minimumAvailable() - let expectedMOET = yieldAvail > 0.0 - ? yieldToMoetSwapper.quoteOut(forProvided: yieldAvail, reverse: false).outAmount + let expectedPyusd0 = yieldAvail > 0.0 + ? yieldToPyusd0Swapper.quoteOut(forProvided: yieldAvail, reverse: false).outAmount : 0.0 - if expectedMOET < totalDebtAmount { - let collateralToMoetSwapper = self._buildCollateralToDebtSwapper( + if expectedPyusd0 < totalDebtAmount { + let collateralToPyusd0Swapper = self._buildCollateralToDebtSwapper( collateralConfig: closeCollateralConfig, tokens: closeTokens, collateralType: collateralType, uniqueID: self.uniqueID! ) - let shortfall = totalDebtAmount - expectedMOET - let quote = collateralToMoetSwapper.quoteIn(forDesired: shortfall, reverse: false) + let shortfall = totalDebtAmount - expectedPyusd0 + let quote = collateralToPyusd0Swapper.quoteIn(forDesired: shortfall, reverse: false) assert(quote.inAmount > 0.0, - message: "Pre-supplement: collateral→MOET quote returned zero input for non-zero shortfall — swapper misconfigured") + message: "Pre-supplement: collateral→PYUSD0 quote returned zero input for non-zero shortfall — swapper misconfigured") let extraCollateral <- self.source.withdrawAvailable(maxAmount: quote.inAmount) assert(extraCollateral.balance > 0.0, - message: "Pre-supplement: no collateral available to cover shortfall of \(shortfall) MOET") - let extraMOET <- collateralToMoetSwapper.swap(quote: quote, inVault: <-extraCollateral) - assert(extraMOET.balance >= shortfall, - message: "Pre-supplement: collateral→MOET swap produced less than shortfall: got \(extraMOET.balance), need \(shortfall)") - self.position.deposit(from: <-extraMOET) + message: "Pre-supplement: no collateral available to cover shortfall of \(shortfall) PYUSD0") + let extraPyusd0 <- collateralToPyusd0Swapper.swap(quote: quote, inVault: <-extraCollateral) + assert(extraPyusd0.balance >= shortfall, + message: "Pre-supplement: collateral→PYUSD0 swap produced less than shortfall: got \(extraPyusd0.balance), need \(shortfall)") + self.position.deposit(from: <-extraPyusd0) } - // Step 7: Create a SwapSource that converts yield tokens → MOET for debt repayment. + // Step 7: Create a SwapSource that converts yield tokens → PYUSD0 for debt repayment. // Step 6's pre-supplement ensures remaining debt ≤ yield value, so SwapSource will // use quoteIn(remainingDebt) and pull only the shares needed — not the full balance. - let moetSource = SwapConnectors.SwapSource( - swapper: yieldToMoetSwapper, + let debtSource = SwapConnectors.SwapSource( + swapper: yieldToPyusd0Swapper, source: yieldTokenSource, uniqueID: self.copyID() ) - // Step 8: Close position - pool pulls up to the (now pre-reduced) debt from moetSource - let resultVaults <- self.position.closePosition(repaymentSources: [moetSource]) + // Step 8: Close position - pool pulls up to the (now pre-reduced) debt from debtSource + let resultVaults <- self.position.closePosition(repaymentSources: [debtSource]) // With one collateral type and one debt type, the pool returns at most two vaults: - // the collateral vault and optionally a MOET overpayment dust vault. + // the collateral vault and optionally a PYUSD0 overpayment dust vault. // closePosition returns vaults in dict-iteration order (hash-based), so we cannot // assume the collateral vault is first. Find it by type and convert any non-collateral - // vaults (MOET overpayment dust) back to collateral via reconstructed swapper. - // Reconstruct MOET→YIELD→collateral from CollateralConfig. + // vaults (PYUSD0 overpayment dust) back to collateral via reconstructed swapper. + // Reconstruct PYUSD0→collateral path from CollateralConfig. let debtToCollateralSwapper = self._buildDebtToCollateralSwapper( collateralConfig: closeCollateralConfig, tokens: closeTokens, @@ -426,34 +425,23 @@ access(all) contract FlowYieldVaultsStrategiesV2 { // any surplus shares are still held by the AutoBalancer and are recovered here. let excessShares <- yieldTokenSource.withdrawAvailable(maxAmount: UFix64.max) if excessShares.balance > 0.0 { - let moetQuote = yieldToMoetSwapper.quoteOut(forProvided: excessShares.balance, reverse: false) - if moetQuote.outAmount > 0.0 { - let moetVault <- yieldToMoetSwapper.swap(quote: moetQuote, inVault: <-excessShares) - let collQuote = debtToCollateralSwapper.quoteOut(forProvided: moetVault.balance, reverse: false) - if collQuote.outAmount > 0.0 { - let extraCollateral <- debtToCollateralSwapper.swap(quote: collQuote, inVault: <-moetVault) - collateralVault.deposit(from: <-extraCollateral) - } else { - emit DustBurned( - tokenType: moetVault.getType().identifier, - balance: moetVault.balance, - quoteInType: collQuote.inType.identifier, - quoteOutType: collQuote.outType.identifier, - quoteInAmount: collQuote.inAmount, - quoteOutAmount: collQuote.outAmount, - swapperType: debtToCollateralSwapper.getType().identifier - ) - Burner.burn(<-moetVault) - } + let sharesToCollateral = SwapConnectors.SequentialSwapper( + swappers: [yieldToPyusd0Swapper, debtToCollateralSwapper], + uniqueID: self.copyID() + ) + let quote = sharesToCollateral.quoteOut(forProvided: excessShares.balance, reverse: false) + if quote.outAmount > 0.0 { + let extraCollateral <- sharesToCollateral.swap(quote: quote, inVault: <-excessShares) + collateralVault.deposit(from: <-extraCollateral) } else { emit DustBurned( tokenType: excessShares.getType().identifier, balance: excessShares.balance, - quoteInType: moetQuote.inType.identifier, - quoteOutType: moetQuote.outType.identifier, - quoteInAmount: moetQuote.inAmount, - quoteOutAmount: moetQuote.outAmount, - swapperType: yieldToMoetSwapper.getType().identifier + quoteInType: quote.inType.identifier, + quoteOutType: quote.outType.identifier, + quoteInAmount: quote.inAmount, + quoteOutAmount: quote.outAmount, + swapperType: sharesToCollateral.getType().identifier ) Burner.burn(<-excessShares) } @@ -501,18 +489,21 @@ access(all) contract FlowYieldVaultsStrategiesV2 { return issuer!.getCollateralConfig(strategyType: strategyType, collateralType: collateralType) } - /// Builds a YIELD→MOET MultiSwapper (AMM direct + ERC4626 redeem path). + /// Builds a YIELD→PYUSD0 MultiSwapper (AMM direct + ERC4626 redeem path). + /// PYUSD0 is the underlying asset of the FUSDEV vault and is also the debt token. access(self) fun _buildYieldToDebtSwapper( tokens: FlowYieldVaultsStrategiesV2.TokenBundle, uniqueID: DeFiActions.UniqueIdentifier ): SwapConnectors.MultiSwapper { + // Direct FUSDEV→PYUSD0 via AMM (fee 100) let yieldToDebtAMM = FlowYieldVaultsStrategiesV2._buildUniV3Swapper( - tokenPath: [tokens.yieldTokenEVMAddress, tokens.moetTokenEVMAddress], + tokenPath: [tokens.yieldTokenEVMAddress, tokens.underlying4626AssetEVMAddress], feePath: [100], inVault: tokens.yieldTokenType, - outVault: tokens.moetTokenType, + outVault: tokens.underlying4626AssetType, uniqueID: uniqueID ) + // FUSDEV→PYUSD0 via Morpho ERC4626 redeem (no additional swap needed) let yieldToUnderlying = MorphoERC4626SwapConnectors.Swapper( vaultEVMAddress: tokens.yieldTokenEVMAddress, coa: FlowYieldVaultsStrategiesV2._getCOACapability(), @@ -520,29 +511,18 @@ access(all) contract FlowYieldVaultsStrategiesV2 { uniqueID: uniqueID, isReversed: true ) - let underlyingToDebt = FlowYieldVaultsStrategiesV2._buildUniV3Swapper( - tokenPath: [tokens.underlying4626AssetEVMAddress, tokens.moetTokenEVMAddress], - feePath: [100], - inVault: tokens.underlying4626AssetType, - outVault: tokens.moetTokenType, - uniqueID: uniqueID - ) - let seq = SwapConnectors.SequentialSwapper( - swappers: [yieldToUnderlying, underlyingToDebt], - uniqueID: uniqueID - ) return SwapConnectors.MultiSwapper( inVault: tokens.yieldTokenType, - outVault: tokens.moetTokenType, - swappers: [yieldToDebtAMM, seq], + outVault: tokens.underlying4626AssetType, + swappers: [yieldToDebtAMM, yieldToUnderlying], uniqueID: uniqueID ) } - /// Builds a collateral→MOET UniV3 swapper from CollateralConfig. + /// Builds a collateral→PYUSD0 UniV3 swapper from CollateralConfig. /// Derives the path by reversing yieldToCollateralUniV3AddressPath[1..] (skipping the - /// yield token) and appending MOET, preserving all intermediate hops. - /// e.g. [FUSDEV, PYUSD0, WETH, WBTC] → [WBTC, WETH, PYUSD0, MOET] + /// yield token); PYUSD0 is the underlying asset and the debt token, so no further hop needed. + /// e.g. [FUSDEV, PYUSD0, WETH, WBTC] → [WBTC, WETH, PYUSD0] access(self) fun _buildCollateralToDebtSwapper( collateralConfig: FlowYieldVaultsStrategiesV2.CollateralConfig, tokens: FlowYieldVaultsStrategiesV2.TokenBundle, @@ -552,54 +532,56 @@ access(all) contract FlowYieldVaultsStrategiesV2 { let yieldToCollPath = collateralConfig.yieldToCollateralUniV3AddressPath let yieldToCollFees = collateralConfig.yieldToCollateralUniV3FeePath assert(yieldToCollPath.length >= 2, message: "yieldToCollateral path must have at least 2 elements") - // Build reversed path: iterate yieldToCollPath from last down to index 1 (skip yield token at 0), - // then append MOET. e.g. [FUSDEV, PYUSD0, WETH, WBTC] → [WBTC, WETH, PYUSD0] + MOET + // Build reversed path: iterate yieldToCollPath from last down to index 1 (skip yield token at 0). + // e.g. [FUSDEV, PYUSD0, WETH, WBTC] → [WBTC, WETH, PYUSD0] var collToDebtPath: [EVM.EVMAddress] = [] var collToDebtFees: [UInt32] = [] for i in InclusiveRange(yieldToCollPath.length - 1, 1, step: -1) { collToDebtPath.append(yieldToCollPath[i]) } - collToDebtPath.append(tokens.moetTokenEVMAddress) - // Build reversed fees: iterate from last down to index 1 (skip yield→underlying fee at 0), - // then append PYUSD0→MOET fee (100). e.g. [100, 3000, 3000] → [3000, 3000] + 100 + // Build reversed fees: iterate from last down to index 1 (skip yield→underlying fee at 0). + // e.g. [100, 3000, 3000] → [3000, 3000] for i in InclusiveRange(yieldToCollFees.length - 1, 1, step: -1) { collToDebtFees.append(yieldToCollFees[i]) } - collToDebtFees.append(UInt32(100)) return FlowYieldVaultsStrategiesV2._buildUniV3Swapper( tokenPath: collToDebtPath, feePath: collToDebtFees, inVault: collateralType, - outVault: tokens.moetTokenType, + outVault: tokens.underlying4626AssetType, uniqueID: uniqueID ) } - /// Builds a MOET→collateral SequentialSwapper for dust handling: MOET→YIELD→collateral. + /// Builds a PYUSD0→collateral UniV3 swapper for overpayment dust handling. + /// Uses the yieldToCollateral path[1..] (skipping the yield token at index 0), + /// going directly from PYUSD0 (the debt/underlying token) to collateral. + /// e.g. [FUSDEV, PYUSD0, WETH] fees [100, 3000] → [PYUSD0, WETH] fees [3000] access(self) fun _buildDebtToCollateralSwapper( collateralConfig: FlowYieldVaultsStrategiesV2.CollateralConfig, tokens: FlowYieldVaultsStrategiesV2.TokenBundle, collateralType: Type, uniqueID: DeFiActions.UniqueIdentifier - ): SwapConnectors.SequentialSwapper { - let debtToYieldAMM = FlowYieldVaultsStrategiesV2._buildUniV3Swapper( - tokenPath: [tokens.moetTokenEVMAddress, tokens.yieldTokenEVMAddress], - feePath: [100], - inVault: tokens.moetTokenType, - outVault: tokens.yieldTokenType, - uniqueID: uniqueID - ) - let yieldToCollateral = FlowYieldVaultsStrategiesV2._buildUniV3Swapper( - tokenPath: collateralConfig.yieldToCollateralUniV3AddressPath, - feePath: collateralConfig.yieldToCollateralUniV3FeePath, - inVault: tokens.yieldTokenType, + ): UniswapV3SwapConnectors.Swapper { + let path = collateralConfig.yieldToCollateralUniV3AddressPath + let fees = collateralConfig.yieldToCollateralUniV3FeePath + assert(path.length >= 2, message: "yieldToCollateral path must have at least 2 elements") + // Skip the yield token at index 0; path[1..] starts at PYUSD0 (the underlying/debt token). + var pyusd0ToCollPath: [EVM.EVMAddress] = [] + var pyusd0ToCollFees: [UInt32] = [] + for i in InclusiveRange(1, path.length - 1) { + pyusd0ToCollPath.append(path[i]) + } + for i in InclusiveRange(1, fees.length - 1) { + pyusd0ToCollFees.append(fees[i]) + } + return FlowYieldVaultsStrategiesV2._buildUniV3Swapper( + tokenPath: pyusd0ToCollPath, + feePath: pyusd0ToCollFees, + inVault: tokens.underlying4626AssetType, outVault: collateralType, uniqueID: uniqueID ) - return SwapConnectors.SequentialSwapper( - swappers: [debtToYieldAMM, yieldToCollateral], - uniqueID: uniqueID - ) } access(self) view fun _isPositionClosed(): Bool { @@ -657,36 +639,18 @@ 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 } - // 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 + return ofToken == self.source.getSourceType() ? self.source.minimumAvailable() : 0.0 } /// Deposits up to the inner Sink's capacity from the provided authorized Vault reference. - /// Accepts both the internal collateral type and, when a pre-swap is configured, the original - /// external collateral type (e.g. PYUSD0) — which is swapped to MOET first. /// FLOW cannot be used as collateral — it is the vault's underlying asset (the debt token). access(all) fun deposit(from: auth(FungibleToken.Withdraw) &{FungibleToken.Vault}) { pre { - from.getType() == self.sink.getSinkType() - || (self.uniqueID != nil && FlowYieldVaultsStrategiesV2._getOriginalCollateralType(self.uniqueID!.id) == from.getType()): + 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 @@ -697,61 +661,18 @@ 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 = self._buildCollateralToMoetSwapper(uniqueID: id) { - let incoming <- from.withdraw(amount: from.balance) - if incoming.balance > 0.0 { - let quote = preSwapper.quoteOut(forProvided: incoming.balance, reverse: false) - assert(quote.outAmount > 0.0, message: "syWFLOWvStrategy deposit: collateral→MOET quote returned zero — check pool liquidity or path config") - let moetVault <- preSwapper.swap(quote: quote, inVault: <-incoming) - assert(moetVault.balance > 0.0, message: "syWFLOWvStrategy deposit: collateral→MOET swap produced zero output") - self.sink.depositCapacity(from: &moetVault as auth(FungibleToken.Withdraw) &{FungibleToken.Vault}) - Burner.burn(<-moetVault) - } else { - Burner.burn(<-incoming) - } - return - } - } - } self.sink.depositCapacity(from: from) } /// Withdraws up to the max amount, returning the withdrawn Vault. If the requested token type is unsupported, /// an empty Vault is returned. - /// - /// For the PYUSD0 pre-swap case: the internal source type is MOET but the external collateral - /// type is PYUSD0. We convert MOET→PYUSD0 via the moetToCollateralSwapper. access(FungibleToken.Withdraw) fun withdraw(maxAmount: UFix64, ofToken: Type): @{FungibleToken.Vault} { - let effectiveSourceType = self.source.getSourceType() - if ofToken != effectiveSourceType { - // For pre-swap case: ofToken is external collateral (e.g. PYUSD0), source is MOET. - if let id = self.uniqueID { - if let originalType = FlowYieldVaultsStrategiesV2._getOriginalCollateralType(id.id) { - if ofToken == originalType { - if let moetToOrigSwapper = self._buildMoetToCollateralSwapper(uniqueID: 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) - } - } - } - } - } + if ofToken != self.source.getSourceType() { return <- DeFiActionsUtils.getEmptyVault(ofToken) } return <- self.source.withdrawAvailable(maxAmount: maxAmount) } /// Closes the underlying FlowALP position by preparing FLOW repayment funds from AutoBalancer - /// (via the stored yield→FLOW swapper) and closing with them. When a stablecoin pre-swap is - /// configured (e.g. PYUSD0→MOET), the returned MOET collateral is swapped back to the - /// original stablecoin before returning. + /// (via the stored yield→FLOW swapper) and closing with them. access(FungibleToken.Withdraw) fun closePosition(collateralType: Type): @{FungibleToken.Vault} { pre { self.isSupportedCollateralType(collateralType): @@ -761,14 +682,6 @@ 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() @@ -796,32 +709,15 @@ access(all) contract FlowYieldVaultsStrategiesV2 { } var collateralVault <- resultVaults.removeFirst() destroy resultVaults - // Convert internal collateral (MOET) → external collateral (e.g. PYUSD0) if needed - if internalCollateralType != collateralType { - let id = self.uniqueID! - let moetToOrigSwapper = self._buildMoetToCollateralSwapper(uniqueID: id) - ?? panic("closePosition: no MOET→collateral swapper for \(collateralType.identifier)") - assert(collateralVault.balance > 0.0, message: "closePosition: zero-debt MOET vault has zero balance") - let quote = moetToOrigSwapper.quoteOut(forProvided: collateralVault.balance, reverse: false) - assert(quote.outAmount > 0.0, message: "closePosition: MOET→\(collateralType.identifier) quote returned zero") - let extVault <- moetToOrigSwapper.swap(quote: quote, inVault: <-collateralVault) - self.positionClosed = true - return <- extVault - } self.positionClosed = true return <- collateralVault } // Step 3: Reconstruct MoreERC4626CollateralConfig and swappers from contract-level config. - // For the stablecoin path (e.g. PYUSD0 → MOET pre-swap), _getOriginalCollateralType - // gives the user-facing key for the MoreERC4626CollateralConfig lookup. - let closeCollateralKey = self.uniqueID != nil - ? FlowYieldVaultsStrategiesV2._getOriginalCollateralType(self.uniqueID!.id) ?? collateralType - : collateralType let closeConfig = self._getStoredMoreERC4626Config( strategyType: Type<@syWFLOWvStrategy>(), - collateralType: closeCollateralKey - ) ?? panic("No MoreERC4626CollateralConfig for syWFLOWvStrategy with \(closeCollateralKey.identifier)") + collateralType: collateralType + ) ?? panic("No MoreERC4626CollateralConfig for syWFLOWvStrategy with \(collateralType.identifier)") let closeTokens = FlowYieldVaultsStrategiesV2._resolveTokenBundle( yieldTokenEVMAddress: closeConfig.yieldTokenEVMAddress ) @@ -833,7 +729,7 @@ access(all) contract FlowYieldVaultsStrategiesV2 { let flowToCollateral = self._buildFlowToCollateralSwapper( closeConfig: closeConfig, closeTokens: closeTokens, - internalCollateralType: internalCollateralType, + collateralType: collateralType, uniqueID: self.uniqueID! ) @@ -886,10 +782,10 @@ access(all) contract FlowYieldVaultsStrategiesV2 { // closePosition returns vaults in dict-iteration order (hash-based), so we cannot // assume the collateral vault is first. Iterate all vaults: collect collateral by type // and convert any non-collateral vaults (FLOW overpayment dust) back to collateral. - var collateralVault <- DeFiActionsUtils.getEmptyVault(internalCollateralType) + var collateralVault <- DeFiActionsUtils.getEmptyVault(collateralType) while resultVaults.length > 0 { let v <- resultVaults.removeFirst() - if v.getType() == internalCollateralType { + if v.getType() == collateralType { collateralVault.deposit(from: <-v) } else if v.balance == 0.0 { // destroy empty vault @@ -923,34 +819,23 @@ access(all) contract FlowYieldVaultsStrategiesV2 { // any surplus shares are still held by the AutoBalancer and are recovered here. let excessShares <- yieldTokenSource.withdrawAvailable(maxAmount: UFix64.max) if excessShares.balance > 0.0 { - let flowQuote = syWFLOWvToFlow.quoteOut(forProvided: excessShares.balance, reverse: false) - if flowQuote.outAmount > 0.0 { - let flowVault <- syWFLOWvToFlow.swap(quote: flowQuote, inVault: <-excessShares) - let collQuote = flowToCollateral.quoteOut(forProvided: flowVault.balance, reverse: false) - if collQuote.outAmount > 0.0 { - let extraCollateral <- flowToCollateral.swap(quote: collQuote, inVault: <-flowVault) - collateralVault.deposit(from: <-extraCollateral) - } else { - emit DustBurned( - tokenType: flowVault.getType().identifier, - balance: flowVault.balance, - quoteInType: collQuote.inType.identifier, - quoteOutType: collQuote.outType.identifier, - quoteInAmount: collQuote.inAmount, - quoteOutAmount: collQuote.outAmount, - swapperType: flowToCollateral.getType().identifier - ) - Burner.burn(<-flowVault) - } + let sharesToCollateral = SwapConnectors.SequentialSwapper( + swappers: [syWFLOWvToFlow, flowToCollateral], + uniqueID: self.copyID() + ) + let quote = sharesToCollateral.quoteOut(forProvided: excessShares.balance, reverse: false) + if quote.outAmount > 0.0 { + let extraCollateral <- sharesToCollateral.swap(quote: quote, inVault: <-excessShares) + collateralVault.deposit(from: <-extraCollateral) } else { emit DustBurned( tokenType: excessShares.getType().identifier, balance: excessShares.balance, - quoteInType: flowQuote.inType.identifier, - quoteOutType: flowQuote.outType.identifier, - quoteInAmount: flowQuote.inAmount, - quoteOutAmount: flowQuote.outAmount, - swapperType: syWFLOWvToFlow.getType().identifier + quoteInType: quote.inType.identifier, + quoteOutType: quote.outType.identifier, + quoteInAmount: quote.inAmount, + quoteOutAmount: quote.outAmount, + swapperType: sharesToCollateral.getType().identifier ) Burner.burn(<-excessShares) } @@ -958,19 +843,6 @@ access(all) contract FlowYieldVaultsStrategiesV2 { Burner.burn(<-excessShares) } - // Convert internal collateral (MOET) → external collateral (e.g. PYUSD0) if needed - if internalCollateralType != collateralType { - let id = self.uniqueID! - let moetToOrigSwapper = self._buildMoetToCollateralSwapper(uniqueID: id) - ?? panic("closePosition: no MOET→collateral swapper for \(collateralType.identifier)") - assert(collateralVault.balance > 0.0, message: "closePosition: MOET vault has zero balance after close") - let quote = moetToOrigSwapper.quoteOut(forProvided: collateralVault.balance, reverse: false) - assert(quote.outAmount > 0.0, message: "closePosition: MOET→\(collateralType.identifier) quote returned zero") - let extVault <- moetToOrigSwapper.swap(quote: quote, inVault: <-collateralVault) - self.positionClosed = true - return <- extVault - } - self.positionClosed = true return <- collateralVault } @@ -978,10 +850,6 @@ access(all) contract FlowYieldVaultsStrategiesV2 { access(contract) fun burnCallback() { FlowYieldVaultsAutoBalancersV1._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) - } } access(all) fun getComponentInfo(): DeFiActions.ComponentInfo { return DeFiActions.ComponentInfo( @@ -1030,93 +898,18 @@ access(all) contract FlowYieldVaultsStrategiesV2 { ) } - /// Builds a collateral→MOET UniV3 swapper from MoetPreswapConfig for the deposit pre-swap. - /// Returns nil if no preswap is configured for this position (non-stablecoin collateral). - access(self) fun _buildCollateralToMoetSwapper( - uniqueID: DeFiActions.UniqueIdentifier - ): UniswapV3SwapConnectors.Swapper? { - if let origType = FlowYieldVaultsStrategiesV2._getOriginalCollateralType(uniqueID.id) { - if let preswapCfg = FlowYieldVaultsStrategiesV2._getMoetPreswapConfig( - composer: Type<@MoreERC4626StrategyComposer>(), - collateral: origType - ) { - let moetType = FlowYieldVaultsStrategiesV2._getPoolDefaultToken() - return FlowYieldVaultsStrategiesV2._buildUniV3Swapper( - tokenPath: preswapCfg.collateralToMoetAddressPath, - feePath: preswapCfg.collateralToMoetFeePath, - inVault: origType, - outVault: moetType, - uniqueID: uniqueID - ) - } - } - return nil - } - - /// Builds a MOET→originalCollateral UniV3 swapper from MoetPreswapConfig (reversed path). - /// Returns nil if no preswap is configured for this position (non-stablecoin collateral). - access(self) fun _buildMoetToCollateralSwapper( - uniqueID: DeFiActions.UniqueIdentifier - ): UniswapV3SwapConnectors.Swapper? { - if let origType = FlowYieldVaultsStrategiesV2._getOriginalCollateralType(uniqueID.id) { - if let preswapCfg = FlowYieldVaultsStrategiesV2._getMoetPreswapConfig( - composer: Type<@MoreERC4626StrategyComposer>(), - collateral: origType - ) { - let moetType = FlowYieldVaultsStrategiesV2._getPoolDefaultToken() - return FlowYieldVaultsStrategiesV2._buildUniV3Swapper( - tokenPath: preswapCfg.collateralToMoetAddressPath.reverse(), - feePath: preswapCfg.collateralToMoetFeePath.reverse(), - inVault: moetType, - outVault: origType, - uniqueID: uniqueID - ) - } - } - return nil - } - - /// Builds a FLOW→internalCollateral UniV3 swapper from MoreERC4626CollateralConfig. - /// Handles both the standard path (FLOW→WBTC/WETH) and the stablecoin path (FLOW→PYUSD0→MOET). + /// Builds a FLOW→collateral UniV3 swapper from MoreERC4626CollateralConfig. access(self) fun _buildFlowToCollateralSwapper( closeConfig: FlowYieldVaultsStrategiesV2.MoreERC4626CollateralConfig, closeTokens: FlowYieldVaultsStrategiesV2.TokenBundle, - internalCollateralType: Type, + collateralType: Type, uniqueID: DeFiActions.UniqueIdentifier ): UniswapV3SwapConnectors.Swapper { - // Stablecoin path (e.g. PYUSD0→MOET pre-swap): extend the FLOW→PYUSD0 path with a PYUSD0→MOET hop. - if let origType = FlowYieldVaultsStrategiesV2._getOriginalCollateralType(uniqueID.id) { - if let preswapCfg = FlowYieldVaultsStrategiesV2._getMoetPreswapConfig( - composer: Type<@MoreERC4626StrategyComposer>(), - collateral: origType - ) { - // Extend the FLOW→collateral path with collateralToMoet[1..] (skip collateral address, - // already the last element of debtToCollateral) and all collateralToMoet fees. - // e.g. debtToCollateral=[WFLOW,PYUSD0], collateralToMoet=[PYUSD0,MOET] - // → [WFLOW, PYUSD0, MOET], fees=[..., 100] - var path = closeConfig.debtToCollateralUniV3AddressPath - for i in InclusiveRange(1, preswapCfg.collateralToMoetAddressPath.length - 1) { - path.append(preswapCfg.collateralToMoetAddressPath[i]) - } - var fees = closeConfig.debtToCollateralUniV3FeePath - for fee in preswapCfg.collateralToMoetFeePath { - fees.append(fee) - } - return FlowYieldVaultsStrategiesV2._buildUniV3Swapper( - tokenPath: path, - feePath: fees, - inVault: closeTokens.underlying4626AssetType, // FLOW - outVault: internalCollateralType, // MOET - uniqueID: uniqueID - ) - } - } - // Standard path (WBTC/WETH): FLOW → collateral directly. return FlowYieldVaultsStrategiesV2._buildUniV3Swapper( tokenPath: closeConfig.debtToCollateralUniV3AddressPath, feePath: closeConfig.debtToCollateralUniV3FeePath, inVault: closeTokens.underlying4626AssetType, // FLOW - outVault: internalCollateralType, + outVault: collateralType, uniqueID: uniqueID ) } @@ -1206,7 +999,8 @@ access(all) contract FlowYieldVaultsStrategiesV2 { } /// Resolves the full token bundle for a strategy given the ERC4626 yield vault address. - /// The MOET token is always the pool's default token. + /// moetTokenType/moetTokenEVMAddress are retained in the struct for upgrade compatibility + /// but are no longer used by active strategy code. access(self) fun _resolveTokenBundle(yieldTokenEVMAddress: EVM.EVMAddress): FlowYieldVaultsStrategiesV2.TokenBundle { let moetTokenType = FlowYieldVaultsStrategiesV2._getPoolDefaultToken() let moetTokenEVMAddress = FlowEVMBridgeConfig.getEVMAddressAssociated(with: moetTokenType) @@ -1381,10 +1175,10 @@ access(all) contract FlowYieldVaultsStrategiesV2 { switch type { // ----------------------------------------------------------------------- - // FUSDEVStrategy: borrows MOET from the FlowALP position, swaps to FUSDEV + // FUSDEVStrategy: borrows PYUSD0 from the FlowALP position, deposits into FUSDEV // ----------------------------------------------------------------------- case Type<@FUSDEVStrategy>(): - // Swappers: MOET <-> YIELD + // Swappers: PYUSD0 (underlying/debt) <-> YIELD (FUSDEV) let debtToYieldSwapper = self._createDebtToYieldSwapper(tokens: tokens, uniqueID: uniqueID) let yieldToDebtSwapper = self._createYieldToDebtSwapper(tokens: tokens, uniqueID: uniqueID) @@ -1503,25 +1297,16 @@ access(all) contract FlowYieldVaultsStrategiesV2 { tokens: FlowYieldVaultsStrategiesV2.TokenBundle, uniqueID: DeFiActions.UniqueIdentifier ): SwapConnectors.MultiSwapper { - // Direct MOET -> YIELD via AMM + // Direct PYUSD0 → FUSDEV via AMM (fee 100) let debtToYieldAMM = self._createUniV3Swapper( - tokenPath: [tokens.moetTokenEVMAddress, tokens.yieldTokenEVMAddress], + tokenPath: [tokens.underlying4626AssetEVMAddress, tokens.yieldTokenEVMAddress], feePath: [100], - inVault: tokens.moetTokenType, + inVault: tokens.underlying4626AssetType, outVault: tokens.yieldTokenType, uniqueID: uniqueID ) - // MOET -> UNDERLYING via AMM - let debtToUnderlying = self._createUniV3Swapper( - tokenPath: [tokens.moetTokenEVMAddress, tokens.underlying4626AssetEVMAddress], - feePath: [100], - inVault: tokens.moetTokenType, - outVault: tokens.underlying4626AssetType, - uniqueID: uniqueID - ) - - // UNDERLYING -> YIELD via Morpho ERC4626 vault deposit + // PYUSD0 → FUSDEV via Morpho ERC4626 vault deposit (no AMM swap needed) let underlyingTo4626 = MorphoERC4626SwapConnectors.Swapper( vaultEVMAddress: tokens.yieldTokenEVMAddress, coa: FlowYieldVaultsStrategiesV2._getCOACapability(), @@ -1530,15 +1315,10 @@ access(all) contract FlowYieldVaultsStrategiesV2 { isReversed: false ) - let seq = SwapConnectors.SequentialSwapper( - swappers: [debtToUnderlying, underlyingTo4626], - uniqueID: uniqueID - ) - return SwapConnectors.MultiSwapper( - inVault: tokens.moetTokenType, + inVault: tokens.underlying4626AssetType, outVault: tokens.yieldTokenType, - swappers: [debtToYieldAMM, seq], + swappers: [debtToYieldAMM, underlyingTo4626], uniqueID: uniqueID ) } @@ -1547,16 +1327,16 @@ access(all) contract FlowYieldVaultsStrategiesV2 { tokens: FlowYieldVaultsStrategiesV2.TokenBundle, uniqueID: DeFiActions.UniqueIdentifier ): SwapConnectors.MultiSwapper { - // Direct YIELD -> MOET via AMM + // Direct FUSDEV → PYUSD0 via AMM (fee 100) let yieldToDebtAMM = self._createUniV3Swapper( - tokenPath: [tokens.yieldTokenEVMAddress, tokens.moetTokenEVMAddress], + tokenPath: [tokens.yieldTokenEVMAddress, tokens.underlying4626AssetEVMAddress], feePath: [100], inVault: tokens.yieldTokenType, - outVault: tokens.moetTokenType, + outVault: tokens.underlying4626AssetType, uniqueID: uniqueID ) - // YIELD -> UNDERLYING redeem via MorphoERC4626 vault + // FUSDEV → PYUSD0 via Morpho ERC4626 redeem (no additional AMM swap needed) let yieldToUnderlying = MorphoERC4626SwapConnectors.Swapper( vaultEVMAddress: tokens.yieldTokenEVMAddress, coa: FlowYieldVaultsStrategiesV2._getCOACapability(), @@ -1564,24 +1344,11 @@ access(all) contract FlowYieldVaultsStrategiesV2 { uniqueID: uniqueID, isReversed: true ) - // UNDERLYING -> MOET via AMM - let underlyingToDebt = self._createUniV3Swapper( - tokenPath: [tokens.underlying4626AssetEVMAddress, tokens.moetTokenEVMAddress], - feePath: [100], - inVault: tokens.underlying4626AssetType, - outVault: tokens.moetTokenType, - uniqueID: uniqueID - ) - - let seq = SwapConnectors.SequentialSwapper( - swappers: [yieldToUnderlying, underlyingToDebt], - uniqueID: uniqueID - ) return SwapConnectors.MultiSwapper( inVault: tokens.yieldTokenType, - outVault: tokens.moetTokenType, - swappers: [yieldToDebtAMM, seq], + outVault: tokens.underlying4626AssetType, + swappers: [yieldToDebtAMM, yieldToUnderlying], uniqueID: uniqueID ) } @@ -1649,13 +1416,12 @@ access(all) contract FlowYieldVaultsStrategiesV2 { ) } - /// Creates a Collateral → Debt (MOET) swapper using UniswapV3. - /// Path: collateral → underlying (PYUSD0) → MOET + /// Creates a Collateral → Debt (PYUSD0) swapper using UniswapV3. + /// Path: collateral → underlying (PYUSD0) /// - /// The fee for collateral→underlying is the last fee in yieldToCollateral (reversed), - /// and the fee for underlying→MOET is fixed at 100 (0.01%, matching yieldToDebtSwapper). - /// Stored and used by FUSDEVStrategy.closePosition to pre-reduce position debt from - /// collateral when yield tokens alone cannot cover the full outstanding MOET debt. + /// The fee for collateral→underlying is the last fee in yieldToCollateral (reversed). + /// Used by FUSDEVStrategy.closePosition to pre-reduce position debt from + /// collateral when yield tokens alone cannot cover the full outstanding PYUSD0 debt. /// access(self) fun _createCollateralToDebtSwapper( collateralConfig: FlowYieldVaultsStrategiesV2.CollateralConfig, @@ -1667,21 +1433,19 @@ access(all) contract FlowYieldVaultsStrategiesV2 { let yieldToCollFees = collateralConfig.yieldToCollateralUniV3FeePath // collateral EVM address = last element of yieldToCollateral path - // underlying (PYUSD0) EVM address = second element of yieldToCollateral path + // underlying (PYUSD0) EVM address = second element of yieldToCollateral path (index 1) assert(yieldToCollPath.length >= 2, message: "yieldToCollateral path must have at least 2 elements") let collateralEVMAddress = yieldToCollPath[yieldToCollPath.length - 1] let underlyingEVMAddress = tokens.underlying4626AssetEVMAddress - // fee[0] = collateral→underlying = last fee in yieldToCollateral (reversed) - // fee[1] = underlying→MOET = 100 (0.01%, matching _createYieldToDebtSwapper) + // fee = collateral→PYUSD0 = last fee in yieldToCollateral (reversed) let collateralToUnderlyingFee = yieldToCollFees[yieldToCollFees.length - 1] - let underlyingToDebtFee: UInt32 = 100 return self._createUniV3Swapper( - tokenPath: [collateralEVMAddress, underlyingEVMAddress, tokens.moetTokenEVMAddress], - feePath: [collateralToUnderlyingFee, underlyingToDebtFee], + tokenPath: [collateralEVMAddress, underlyingEVMAddress], + feePath: [collateralToUnderlyingFee], inVault: collateralType, - outVault: tokens.moetTokenType, + outVault: tokens.underlying4626AssetType, uniqueID: uniqueID ) } @@ -1772,7 +1536,7 @@ access(all) contract FlowYieldVaultsStrategiesV2 { uniqueID: uniqueID ) - // For syWFLOWvStrategy the debt token IS the underlying asset (FLOW), not MOET. + // For syWFLOWvStrategy the debt token IS the underlying asset (FLOW). let flowDebtTokenType = tokens.underlying4626AssetType // FLOW → syWFLOWv: standard ERC4626 deposit (More vault, not Morpho — no AMM needed) @@ -2342,11 +2106,6 @@ access(all) contract FlowYieldVaultsStrategiesV2 { self.IssuerStoragePath = StoragePath(identifier: "FlowYieldVaultsStrategyV2ComposerIssuer_\(self.account.address)")! self.config = {} - let moetType = Type<@MOET.Vault>() - if FlowEVMBridgeConfig.getEVMAddressAssociated(with: Type<@MOET.Vault>()) == nil { - panic("Could not find EVM address for \(moetType.identifier) - ensure the asset is onboarded to the VM Bridge") - } - let issuer <- create StrategyComposerIssuer( configs: { Type<@MorphoERC4626StrategyComposer>(): { diff --git a/cadence/tests/FlowYieldVaultsStrategiesV2_FUSDEV_test.cdc b/cadence/tests/FlowYieldVaultsStrategiesV2_FUSDEV_test.cdc index ae4573fb..ae88d2c6 100644 --- a/cadence/tests/FlowYieldVaultsStrategiesV2_FUSDEV_test.cdc +++ b/cadence/tests/FlowYieldVaultsStrategiesV2_FUSDEV_test.cdc @@ -42,6 +42,10 @@ access(all) let adminAccount = Test.getAccount(0xb1d63873c3cc9f79) /// Used for WFLOW lifecycle tests and for the negative PYUSD0 collateral test. access(all) let flowUser = Test.getAccount(0x443472749ebdaac8) +/// Large PYUSD0 holder (~70k PYUSD0) — used solely to seed the FlowALP pool's +/// PYUSD0 reserves so the pool can service PYUSD0 drawdowns for FUSDEVStrategy positions. +access(all) let pyusd0Holder = Test.getAccount(0x24263c125b7770e0) + /// FlowToken contract account — used to provision FLOW to flowUser in setup. access(all) let flowTokenAccount = Test.getAccount(0x1654653399040a61) @@ -310,6 +314,28 @@ access(all) fun setup() { ) Test.expect(result, Test.beSucceeded()) + // Seed the FlowALP pool with PYUSD0 reserves. + // FUSDEVStrategy borrows PYUSD0 as its debt token (drawDownSink expects PYUSD0). + // The pool can mint MOET but must draw non-MOET tokens from reserves[tokenType]. + // pyusd0Holder (0x24263c125b7770e0) holds ~70k PYUSD0 on mainnet — grant pool + // access and have them deposit 1000 PYUSD0 so the pool can service drawdowns. + let alpAdmin = Test.getAccount(0x6b00ff876c299c61) + log("Granting pyusd0Holder FlowALP pool cap for PYUSD0 reserve position...") + result = _executeTransactionFile( + "../../lib/FlowALP/cadence/tests/transactions/flow-alp/pool-management/03_grant_beta.cdc", + [], + [alpAdmin, pyusd0Holder] + ) + Test.expect(result, Test.beSucceeded()) + + log("Creating 1000 PYUSD0 reserve position in FlowALP pool (pushToDrawDownSink: false)...") + result = _executeTransactionFile( + "../../lib/FlowALP/cadence/transactions/flow-alp/position/create_position.cdc", + [1000.0 as UFix64, /storage/EVMVMBridgedToken_99af3eea856556646c98c8b9b2548fe815240750Vault as StoragePath, false as Bool], + [pyusd0Holder] + ) + Test.expect(result, Test.beSucceeded()) + // Provision extra FLOW to flowUser so that testDepositToFUSDEVYieldVault_WFLOW has enough balance. // flowUser starts with ~11 FLOW; the create uses 10.0, leaving ~1 FLOW — not enough for a 5.0 deposit. log("Provisioning 20.0 FLOW to WFLOW user from FlowToken contract account...") From ecb1c63b4baca8062e432ab04a041c2ffa6ec97d Mon Sep 17 00:00:00 2001 From: Alex <12097569+nialexsan@users.noreply.github.com> Date: Mon, 30 Mar 2026 21:37:12 -0400 Subject: [PATCH 50/72] backcompat test --- cadence/tests/backcompat_redeploy_test.cdc | 397 +++++++++++++++++++++ 1 file changed, 397 insertions(+) create mode 100644 cadence/tests/backcompat_redeploy_test.cdc diff --git a/cadence/tests/backcompat_redeploy_test.cdc b/cadence/tests/backcompat_redeploy_test.cdc new file mode 100644 index 00000000..a6920aa5 --- /dev/null +++ b/cadence/tests/backcompat_redeploy_test.cdc @@ -0,0 +1,397 @@ +#test_fork(network: "mainnet", height: nil) + +import Test + +import "EVM" +import "FlowToken" +import "FlowYieldVaults" +import "FlowYieldVaultsClosedBeta" +import "AutoBalancerCallbackWrapper" +import "FlowYieldVaultsAutoBalancers" +import "FlowYieldVaultsSchedulerRegistry" +import "FlowYieldVaultsSchedulerV1" +import "FlowYieldVaultsStrategiesV2" +import "PMStrategiesV1" + +/// Backward-compatibility fork test. +/// +/// Simulates a full contract upgrade by redeploying every contract in the +/// FlowYieldVaults ecosystem (and their dependencies) from the local codebase +/// on top of the live mainnet fork state. +/// +/// The test verifies that: +/// 1. All contracts can be redeployed without panicking. +/// 2. Pre-existing on-chain state (accounts, stored resources) is still +/// accessible after the upgrade. +/// 3. Core read-only operations (beta status, strategy list, vault IDs, +/// scheduler registry) continue to return sensible results. +/// 4. The admin StrategyComposerIssuer is still accessible and can issue +/// composers after the upgrade. +/// +/// Mainnet addresses: +/// Admin / deployer : 0xb1d63873c3cc9f79 +/// PYUSD0 test user : 0x443472749ebdaac8 +/// WBTC/WETH user : 0x68da18f20e98a7b6 + +// --- Accounts --- + +access(all) let adminAccount = Test.getAccount(0xb1d63873c3cc9f79) +access(all) let bandOracleAdmin = Test.getAccount(0x6801a6222ebf784a) +access(all) let pyusd0User = Test.getAccount(0x443472749ebdaac8) +access(all) let wbtcWethUser = Test.getAccount(0x68da18f20e98a7b6) + +// --- UniV3 addresses (mainnet) --- + +access(all) let univ3Factory = "0xca6d7Bb03334bBf135902e1d919a5feccb461632" +access(all) let univ3Router = "0xeEDC6Ff75e1b10B903D9013c358e446a73d35341" +access(all) let univ3Quoter = "0x370A8DF17742867a44e56223EC20D82092242C85" + +/* --- 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) +} + +/* --- Setup: redeploy every contract from local source --- */ + +access(all) fun setup() { + log("==== Backward-Compatibility Redeploy Test: Setup ====") + + // ------------------------------------------------------------------ // + // 1. FlowALP / FlowActions dependency contracts + // ------------------------------------------------------------------ // + + 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 ERC4626SinkConnectors...") + err = Test.deployContract( + name: "ERC4626SinkConnectors", + path: "../../lib/FlowALP/FlowActions/cadence/contracts/connectors/evm/ERC4626SinkConnectors.cdc", + arguments: [] + ) + Test.expect(err, Test.beNil()) + + 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 ERC4626PriceOracles...") + err = Test.deployContract( + name: "ERC4626PriceOracles", + path: "../../lib/FlowALP/FlowActions/cadence/contracts/connectors/evm/ERC4626PriceOracles.cdc", + arguments: [] + ) + Test.expect(err, Test.beNil()) + + // ------------------------------------------------------------------ // + // 2. Core FlowYieldVaults platform contracts + // (deployed at 0xb1d63873c3cc9f79 on mainnet) + // ------------------------------------------------------------------ // + + log("Deploying UInt64LinkedList...") + err = Test.deployContract( + name: "UInt64LinkedList", + path: "../../cadence/contracts/UInt64LinkedList.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 FlowYieldVaultsClosedBeta...") + err = Test.deployContract( + name: "FlowYieldVaultsClosedBeta", + path: "../../cadence/contracts/FlowYieldVaultsClosedBeta.cdc", + arguments: [] + ) + Test.expect(err, Test.beNil()) + + log("Deploying AutoBalancerCallbackWrapper...") + err = Test.deployContract( + name: "AutoBalancerCallbackWrapper", + path: "../../cadence/contracts/AutoBalancerCallbackWrapper.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 FlowYieldVaultsSchedulerRegistry...") + err = Test.deployContract( + name: "FlowYieldVaultsSchedulerRegistry", + path: "../../cadence/contracts/FlowYieldVaultsSchedulerRegistry.cdc", + arguments: [] + ) + Test.expect(err, Test.beNil()) + + log("Deploying FlowYieldVaultsSchedulerV1...") + err = Test.deployContract( + name: "FlowYieldVaultsSchedulerV1", + path: "../../cadence/contracts/FlowYieldVaultsSchedulerV1.cdc", + arguments: [] + ) + Test.expect(err, Test.beNil()) + + log("Deploying FlowYieldVaultsStrategiesV2...") + err = Test.deployContract( + name: "FlowYieldVaultsStrategiesV2", + path: "../../cadence/contracts/FlowYieldVaultsStrategiesV2.cdc", + arguments: [univ3Factory, univ3Router, univ3Quoter] + ) + Test.expect(err, Test.beNil()) + + log("Deploying PMStrategiesV1...") + err = Test.deployContract( + name: "PMStrategiesV1", + path: "../../cadence/contracts/PMStrategiesV1.cdc", + arguments: [univ3Factory, univ3Router, univ3Quoter] + ) + Test.expect(err, Test.beNil()) + + log("==== All contracts redeployed successfully ====") +} + +/* --- Tests --- */ + +/// All contracts deployed without error — already asserted in setup(). +/// This test serves as the initial smoke-check gate. +access(all) fun testAllContractsRedeployedWithoutError() { + log("All contracts redeployed without error (verified in setup)") +} + +/// Verify that FlowYieldVaults is accessible and returns supported strategies +/// after the upgrade. +access(all) fun testSupportedStrategiesReadable() { + log("Checking FlowYieldVaults.getSupportedStrategies() after redeploy...") + let result = _executeScript( + "../scripts/flow-yield-vaults/get_supported_strategies.cdc", + [] + ) + Test.expect(result, Test.beSucceeded()) + log("Supported strategies readable after redeploy") +} + +/// Verify that the SchedulerRegistry state is still readable after the upgrade. +access(all) fun testSchedulerRegistryReadable() { + log("Checking FlowYieldVaultsSchedulerRegistry state...") + let result = _executeScript( + "../scripts/flow-yield-vaults/get_registered_yield_vault_count.cdc", + [] + ) + Test.expect(result, Test.beSucceeded()) + let count = result.returnValue! as! Int + log("Registered yield vault count after redeploy: ".concat(count.toString())) +} + +/// Verify that the pending vault queue is still intact after the upgrade. +access(all) fun testPendingQueueReadable() { + log("Checking pending yield vault queue...") + let result = _executeScript( + "../scripts/flow-yield-vaults/get_pending_count.cdc", + [] + ) + Test.expect(result, Test.beSucceeded()) + let count = result.returnValue! as! Int + log("Pending vault count after redeploy: ".concat(count.toString())) +} + +/// Verify that existing yield vault IDs for the PYUSD0 user are still readable. +access(all) fun testExistingPyusd0UserVaultsReadable() { + log("Checking PYUSD0 user yield vault IDs after redeploy...") + let result = _executeScript( + "../scripts/flow-yield-vaults/get_yield_vault_ids.cdc", + [pyusd0User.address] + ) + Test.expect(result, Test.beSucceeded()) + let ids = result.returnValue! as! [UInt64]? + if ids == nil || ids!.length == 0 { + log("PYUSD0 user has no existing yield vaults on mainnet (OK for closed beta)") + } else { + log("PYUSD0 user existing vault IDs: ".concat(ids!.length.toString()).concat(" vaults found")) + // Verify balance is readable for each vault + for id in ids! { + let balResult = _executeScript( + "../scripts/flow-yield-vaults/get_yield_vault_balance.cdc", + [pyusd0User.address, id] + ) + Test.expect(balResult, Test.beSucceeded()) + log(" Vault ".concat(id.toString()).concat(" balance readable")) + } + } +} + +/// Verify that existing yield vault IDs for the WBTC/WETH user are still readable. +access(all) fun testExistingWbtcWethUserVaultsReadable() { + log("Checking WBTC/WETH user yield vault IDs after redeploy...") + let result = _executeScript( + "../scripts/flow-yield-vaults/get_yield_vault_ids.cdc", + [wbtcWethUser.address] + ) + Test.expect(result, Test.beSucceeded()) + let ids = result.returnValue! as! [UInt64]? + if ids == nil || ids!.length == 0 { + log("WBTC/WETH user has no existing yield vaults on mainnet (OK for closed beta)") + } else { + log("WBTC/WETH user existing vault IDs: ".concat(ids!.length.toString()).concat(" vaults found")) + for id in ids! { + let balResult = _executeScript( + "../scripts/flow-yield-vaults/get_yield_vault_balance.cdc", + [wbtcWethUser.address, id] + ) + Test.expect(balResult, Test.beSucceeded()) + log(" Vault ".concat(id.toString()).concat(" balance readable")) + } + } +} + +/// Verify that existing yield vault IDs for the admin account are still readable. +access(all) fun testExistingAdminVaultsReadable() { + log("Checking admin yield vault IDs after redeploy...") + let result = _executeScript( + "../scripts/flow-yield-vaults/get_yield_vault_ids.cdc", + [adminAccount.address] + ) + Test.expect(result, Test.beSucceeded()) + let ids = result.returnValue! as! [UInt64]? + if ids == nil || ids!.length == 0 { + log("Admin has no existing yield vaults (OK)") + } else { + log("Admin existing vault IDs: ".concat(ids!.length.toString()).concat(" vaults found")) + for id in ids! { + let balResult = _executeScript( + "../scripts/flow-yield-vaults/get_yield_vault_balance.cdc", + [adminAccount.address, id] + ) + Test.expect(balResult, Test.beSucceeded()) + log(" Vault ".concat(id.toString()).concat(" balance readable")) + } + } +} + +/// Verify that the closed beta gate still functions correctly after the upgrade. +access(all) fun testClosedBetaStatePreserved() { + log("Checking closed beta state after redeploy...") + let betaCapResult = _executeScript( + "../scripts/flow-yield-vaults/get_beta_cap.cdc", + [adminAccount.address] + ) + Test.expect(betaCapResult, Test.beSucceeded()) + log("Beta cap accessible after redeploy") + + let activeCountResult = _executeScript( + "../scripts/flow-yield-vaults/get_active_beta_count.cdc", + [] + ) + Test.expect(activeCountResult, Test.beSucceeded()) + let count = activeCountResult.returnValue! as! Int + log("Active beta user count after redeploy: ".concat(count.toString())) +} + +/// Verify that the PMStrategiesV1 IssuerStoragePath is still accessible after +/// the upgrade and that strategy configuration can be upserted. +access(all) fun testPMStrategiesV1IssuerAccessible() { + log("Configuring PMStrategiesV1 syWFLOWv strategy for FLOW collateral...") + let result = _executeTransactionFile( + "../transactions/flow-yield-vaults/admin/upsert-pm-strategy-config.cdc", + [ + "A.b1d63873c3cc9f79.PMStrategiesV1.syWFLOWvStrategy", + "A.1654653399040a61.FlowToken.Vault", + "0xCBf9a7753F9D2d0e8141ebB36d99f87AcEf98597", // syWFLOWv + UInt32(100) // WFLOW/syWFLOWv fee tier + ], + [adminAccount] + ) + Test.expect(result, Test.beSucceeded()) + log("PMStrategiesV1 syWFLOWv strategy config upserted successfully") +} + +/// Verify that FlowYieldVaultsStrategiesV2 StrategyComposerIssuer is accessible +/// and can be configured with a new strategy after the upgrade. +access(all) fun testStrategiesV2IssuerConfigurable() { + log("Configuring FlowYieldVaultsStrategiesV2 FUSDEVStrategy for PYUSD0 collateral...") + // PYUSD0 collateral: yield -> collateral path is [FUSDEV_EVM, PYUSD0_EVM], fee 100 + let result = _executeTransactionFile( + "../transactions/flow-yield-vaults/admin/upsert_strategy_config.cdc", + [ + "A.b1d63873c3cc9f79.FlowYieldVaultsStrategiesV2.FUSDEVStrategy", + "A.1e4aa0b87d10b141.EVMVMBridgedToken_99af3eea856556646c98c8b9b2548fe815240750.Vault", + "0xd069d989e2F44B70c65347d1853C0c67e10a9F8D", // FUSDEV yield token + ["0xd069d989e2F44B70c65347d1853C0c67e10a9F8D", + "0x99aF3EeA856556646C98c8B9b2548Fe815240750"], // yield -> PYUSD0 + [UInt32(100)] + ], + [adminAccount] + ) + Test.expect(result, Test.beSucceeded()) + log("FlowYieldVaultsStrategiesV2 FUSDEVStrategy config upserted successfully") +} From 9e22f00512c9796ac2fa9ec0c333aa115dac567c Mon Sep 17 00:00:00 2001 From: Alex <12097569+nialexsan@users.noreply.github.com> Date: Tue, 31 Mar 2026 08:40:14 -0400 Subject: [PATCH 51/72] remove backcompat test --- ...flow_yield_vaults_backward_compat_test.cdc | 109 ------------------ 1 file changed, 109 deletions(-) delete mode 100644 cadence/tests/fork/flow_yield_vaults_backward_compat_test.cdc diff --git a/cadence/tests/fork/flow_yield_vaults_backward_compat_test.cdc b/cadence/tests/fork/flow_yield_vaults_backward_compat_test.cdc deleted file mode 100644 index b7bafa39..00000000 --- a/cadence/tests/fork/flow_yield_vaults_backward_compat_test.cdc +++ /dev/null @@ -1,109 +0,0 @@ -#test_fork(network: "mainnet", height: nil) - -import Test - -/// Backward-compatibility redeploy test for FlowYieldVaults contracts. -/// -/// Redeploys all FlowYieldVaults contracts from the current repo onto the forked mainnet -/// state. A successful redeploy confirms the updated code is backward-compatible with the -/// existing on-chain storage layout and dependent contracts. -/// -/// Deployment order respects import dependencies: -/// UInt64LinkedList -/// → FlowYieldVaultsClosedBeta -/// → FlowYieldVaultsSchedulerRegistryV1 -/// → FlowYieldVaults -/// → FlowYieldVaultsAutoBalancersV1 -/// → FlowYieldVaultsSchedulerV1 -/// → FlowYieldVaultsStrategiesV2 -/// → PMStrategiesV1 - -access(all) struct ContractSpec { - access(all) let path: String - access(all) let arguments: [AnyStruct] - - init(path: String, arguments: [AnyStruct]) { - self.path = path - self.arguments = arguments - } -} - -/// Extracts the contract name from a file path. -/// "../../cadence/contracts/FlowYieldVaults.cdc" → "FlowYieldVaults" -access(all) fun contractNameFromPath(_ path: String): String { - let parts = path.split(separator: "/") - let file = parts[parts.length - 1] - return file.split(separator: ".")[0] -} - -access(all) fun deployAndExpectSuccess(_ spec: ContractSpec) { - let name = contractNameFromPath(spec.path) - log("Deploying ".concat(name).concat("...")) - let err = Test.deployContract(name: name, path: spec.path, arguments: spec.arguments) - Test.expect(err, Test.beNil()) - Test.commitBlock() -} - -// UniV3 mainnet addresses — required by strategy contracts on init -access(all) let univ3Factory = "0xca6d7Bb03334bBf135902e1d919a5feccb461632" -access(all) let univ3Router = "0xeEDC6Ff75e1b10B903D9013c358e446a73d35341" -access(all) let univ3Quoter = "0x370A8DF17742867a44e56223EC20D82092242C85" - -access(all) fun setup() { - log("==== FlowYieldVaults Backward-Compatibility Redeploy Test ====") - - let contracts: [ContractSpec] = [ - ContractSpec( - path: "../../contracts/UInt64LinkedList.cdc", - arguments: [] - ), - ContractSpec( - path: "../../contracts/AutoBalancers.cdc", - arguments: [] - ), - ContractSpec( - path: "../../contracts/FlowYieldVaultsClosedBeta.cdc", - arguments: [] - ), - ContractSpec( - path: "../../contracts/FlowYieldVaultsSchedulerRegistryV1.cdc", - arguments: [] - ), - ContractSpec( - path: "../../contracts/FlowYieldVaults.cdc", - arguments: [] - ), - ContractSpec( - path: "../../contracts/FlowYieldVaultsAutoBalancers.cdc", - arguments: [] - ), - ContractSpec( - path: "../../contracts/FlowYieldVaultsAutoBalancersV1.cdc", - arguments: [] - ), - ContractSpec( - path: "../../contracts/FlowYieldVaultsSchedulerV1.cdc", - arguments: [] - ), - - // @TODO restore in strategies PR - // ContractSpec( - // path: "../../contracts/FlowYieldVaultsStrategiesV2.cdc", - // arguments: [univ3Factory, univ3Router, univ3Quoter] - // ), - ContractSpec( - path: "../../contracts/PMStrategiesV1.cdc", - arguments: [univ3Factory, univ3Router, univ3Quoter] - ) - ] - - for spec in contracts { - deployAndExpectSuccess(spec) - } - - log("==== All FlowYieldVaults contracts redeployed successfully ====") -} - -access(all) fun testAllContractsRedeployedWithoutError() { - log("All FlowYieldVaults contracts redeployed without error (verified in setup)") -} From 8067522760badadcb1625217320d572a7b295adb Mon Sep 17 00:00:00 2001 From: Alex <12097569+nialexsan@users.noreply.github.com> Date: Tue, 31 Mar 2026 08:56:51 -0400 Subject: [PATCH 52/72] restore backcompat test --- ...flow_yield_vaults_backward_compat_test.cdc | 109 ++++++++++++++++++ 1 file changed, 109 insertions(+) create mode 100644 cadence/tests/flow_yield_vaults_backward_compat_test.cdc diff --git a/cadence/tests/flow_yield_vaults_backward_compat_test.cdc b/cadence/tests/flow_yield_vaults_backward_compat_test.cdc new file mode 100644 index 00000000..b7bafa39 --- /dev/null +++ b/cadence/tests/flow_yield_vaults_backward_compat_test.cdc @@ -0,0 +1,109 @@ +#test_fork(network: "mainnet", height: nil) + +import Test + +/// Backward-compatibility redeploy test for FlowYieldVaults contracts. +/// +/// Redeploys all FlowYieldVaults contracts from the current repo onto the forked mainnet +/// state. A successful redeploy confirms the updated code is backward-compatible with the +/// existing on-chain storage layout and dependent contracts. +/// +/// Deployment order respects import dependencies: +/// UInt64LinkedList +/// → FlowYieldVaultsClosedBeta +/// → FlowYieldVaultsSchedulerRegistryV1 +/// → FlowYieldVaults +/// → FlowYieldVaultsAutoBalancersV1 +/// → FlowYieldVaultsSchedulerV1 +/// → FlowYieldVaultsStrategiesV2 +/// → PMStrategiesV1 + +access(all) struct ContractSpec { + access(all) let path: String + access(all) let arguments: [AnyStruct] + + init(path: String, arguments: [AnyStruct]) { + self.path = path + self.arguments = arguments + } +} + +/// Extracts the contract name from a file path. +/// "../../cadence/contracts/FlowYieldVaults.cdc" → "FlowYieldVaults" +access(all) fun contractNameFromPath(_ path: String): String { + let parts = path.split(separator: "/") + let file = parts[parts.length - 1] + return file.split(separator: ".")[0] +} + +access(all) fun deployAndExpectSuccess(_ spec: ContractSpec) { + let name = contractNameFromPath(spec.path) + log("Deploying ".concat(name).concat("...")) + let err = Test.deployContract(name: name, path: spec.path, arguments: spec.arguments) + Test.expect(err, Test.beNil()) + Test.commitBlock() +} + +// UniV3 mainnet addresses — required by strategy contracts on init +access(all) let univ3Factory = "0xca6d7Bb03334bBf135902e1d919a5feccb461632" +access(all) let univ3Router = "0xeEDC6Ff75e1b10B903D9013c358e446a73d35341" +access(all) let univ3Quoter = "0x370A8DF17742867a44e56223EC20D82092242C85" + +access(all) fun setup() { + log("==== FlowYieldVaults Backward-Compatibility Redeploy Test ====") + + let contracts: [ContractSpec] = [ + ContractSpec( + path: "../../contracts/UInt64LinkedList.cdc", + arguments: [] + ), + ContractSpec( + path: "../../contracts/AutoBalancers.cdc", + arguments: [] + ), + ContractSpec( + path: "../../contracts/FlowYieldVaultsClosedBeta.cdc", + arguments: [] + ), + ContractSpec( + path: "../../contracts/FlowYieldVaultsSchedulerRegistryV1.cdc", + arguments: [] + ), + ContractSpec( + path: "../../contracts/FlowYieldVaults.cdc", + arguments: [] + ), + ContractSpec( + path: "../../contracts/FlowYieldVaultsAutoBalancers.cdc", + arguments: [] + ), + ContractSpec( + path: "../../contracts/FlowYieldVaultsAutoBalancersV1.cdc", + arguments: [] + ), + ContractSpec( + path: "../../contracts/FlowYieldVaultsSchedulerV1.cdc", + arguments: [] + ), + + // @TODO restore in strategies PR + // ContractSpec( + // path: "../../contracts/FlowYieldVaultsStrategiesV2.cdc", + // arguments: [univ3Factory, univ3Router, univ3Quoter] + // ), + ContractSpec( + path: "../../contracts/PMStrategiesV1.cdc", + arguments: [univ3Factory, univ3Router, univ3Quoter] + ) + ] + + for spec in contracts { + deployAndExpectSuccess(spec) + } + + log("==== All FlowYieldVaults contracts redeployed successfully ====") +} + +access(all) fun testAllContractsRedeployedWithoutError() { + log("All FlowYieldVaults contracts redeployed without error (verified in setup)") +} From 343bdbec6de3c553afac1752472f2609b196ce1a Mon Sep 17 00:00:00 2001 From: Alex <12097569+nialexsan@users.noreply.github.com> Date: Tue, 31 Mar 2026 08:58:11 -0400 Subject: [PATCH 53/72] restore backcompat test --- ...flow_yield_vaults_backward_compat_test.cdc | 109 ++++++++++++++++++ 1 file changed, 109 insertions(+) create mode 100644 cadence/tests/fork/flow_yield_vaults_backward_compat_test.cdc diff --git a/cadence/tests/fork/flow_yield_vaults_backward_compat_test.cdc b/cadence/tests/fork/flow_yield_vaults_backward_compat_test.cdc new file mode 100644 index 00000000..b7bafa39 --- /dev/null +++ b/cadence/tests/fork/flow_yield_vaults_backward_compat_test.cdc @@ -0,0 +1,109 @@ +#test_fork(network: "mainnet", height: nil) + +import Test + +/// Backward-compatibility redeploy test for FlowYieldVaults contracts. +/// +/// Redeploys all FlowYieldVaults contracts from the current repo onto the forked mainnet +/// state. A successful redeploy confirms the updated code is backward-compatible with the +/// existing on-chain storage layout and dependent contracts. +/// +/// Deployment order respects import dependencies: +/// UInt64LinkedList +/// → FlowYieldVaultsClosedBeta +/// → FlowYieldVaultsSchedulerRegistryV1 +/// → FlowYieldVaults +/// → FlowYieldVaultsAutoBalancersV1 +/// → FlowYieldVaultsSchedulerV1 +/// → FlowYieldVaultsStrategiesV2 +/// → PMStrategiesV1 + +access(all) struct ContractSpec { + access(all) let path: String + access(all) let arguments: [AnyStruct] + + init(path: String, arguments: [AnyStruct]) { + self.path = path + self.arguments = arguments + } +} + +/// Extracts the contract name from a file path. +/// "../../cadence/contracts/FlowYieldVaults.cdc" → "FlowYieldVaults" +access(all) fun contractNameFromPath(_ path: String): String { + let parts = path.split(separator: "/") + let file = parts[parts.length - 1] + return file.split(separator: ".")[0] +} + +access(all) fun deployAndExpectSuccess(_ spec: ContractSpec) { + let name = contractNameFromPath(spec.path) + log("Deploying ".concat(name).concat("...")) + let err = Test.deployContract(name: name, path: spec.path, arguments: spec.arguments) + Test.expect(err, Test.beNil()) + Test.commitBlock() +} + +// UniV3 mainnet addresses — required by strategy contracts on init +access(all) let univ3Factory = "0xca6d7Bb03334bBf135902e1d919a5feccb461632" +access(all) let univ3Router = "0xeEDC6Ff75e1b10B903D9013c358e446a73d35341" +access(all) let univ3Quoter = "0x370A8DF17742867a44e56223EC20D82092242C85" + +access(all) fun setup() { + log("==== FlowYieldVaults Backward-Compatibility Redeploy Test ====") + + let contracts: [ContractSpec] = [ + ContractSpec( + path: "../../contracts/UInt64LinkedList.cdc", + arguments: [] + ), + ContractSpec( + path: "../../contracts/AutoBalancers.cdc", + arguments: [] + ), + ContractSpec( + path: "../../contracts/FlowYieldVaultsClosedBeta.cdc", + arguments: [] + ), + ContractSpec( + path: "../../contracts/FlowYieldVaultsSchedulerRegistryV1.cdc", + arguments: [] + ), + ContractSpec( + path: "../../contracts/FlowYieldVaults.cdc", + arguments: [] + ), + ContractSpec( + path: "../../contracts/FlowYieldVaultsAutoBalancers.cdc", + arguments: [] + ), + ContractSpec( + path: "../../contracts/FlowYieldVaultsAutoBalancersV1.cdc", + arguments: [] + ), + ContractSpec( + path: "../../contracts/FlowYieldVaultsSchedulerV1.cdc", + arguments: [] + ), + + // @TODO restore in strategies PR + // ContractSpec( + // path: "../../contracts/FlowYieldVaultsStrategiesV2.cdc", + // arguments: [univ3Factory, univ3Router, univ3Quoter] + // ), + ContractSpec( + path: "../../contracts/PMStrategiesV1.cdc", + arguments: [univ3Factory, univ3Router, univ3Quoter] + ) + ] + + for spec in contracts { + deployAndExpectSuccess(spec) + } + + log("==== All FlowYieldVaults contracts redeployed successfully ====") +} + +access(all) fun testAllContractsRedeployedWithoutError() { + log("All FlowYieldVaults contracts redeployed without error (verified in setup)") +} From e69ed6ae186aab8713b3f20c84fa92f21d9cd129 Mon Sep 17 00:00:00 2001 From: Alex <12097569+nialexsan@users.noreply.github.com> Date: Tue, 31 Mar 2026 08:58:56 -0400 Subject: [PATCH 54/72] remove duplicate --- ...flow_yield_vaults_backward_compat_test.cdc | 109 ------------------ 1 file changed, 109 deletions(-) delete mode 100644 cadence/tests/flow_yield_vaults_backward_compat_test.cdc diff --git a/cadence/tests/flow_yield_vaults_backward_compat_test.cdc b/cadence/tests/flow_yield_vaults_backward_compat_test.cdc deleted file mode 100644 index b7bafa39..00000000 --- a/cadence/tests/flow_yield_vaults_backward_compat_test.cdc +++ /dev/null @@ -1,109 +0,0 @@ -#test_fork(network: "mainnet", height: nil) - -import Test - -/// Backward-compatibility redeploy test for FlowYieldVaults contracts. -/// -/// Redeploys all FlowYieldVaults contracts from the current repo onto the forked mainnet -/// state. A successful redeploy confirms the updated code is backward-compatible with the -/// existing on-chain storage layout and dependent contracts. -/// -/// Deployment order respects import dependencies: -/// UInt64LinkedList -/// → FlowYieldVaultsClosedBeta -/// → FlowYieldVaultsSchedulerRegistryV1 -/// → FlowYieldVaults -/// → FlowYieldVaultsAutoBalancersV1 -/// → FlowYieldVaultsSchedulerV1 -/// → FlowYieldVaultsStrategiesV2 -/// → PMStrategiesV1 - -access(all) struct ContractSpec { - access(all) let path: String - access(all) let arguments: [AnyStruct] - - init(path: String, arguments: [AnyStruct]) { - self.path = path - self.arguments = arguments - } -} - -/// Extracts the contract name from a file path. -/// "../../cadence/contracts/FlowYieldVaults.cdc" → "FlowYieldVaults" -access(all) fun contractNameFromPath(_ path: String): String { - let parts = path.split(separator: "/") - let file = parts[parts.length - 1] - return file.split(separator: ".")[0] -} - -access(all) fun deployAndExpectSuccess(_ spec: ContractSpec) { - let name = contractNameFromPath(spec.path) - log("Deploying ".concat(name).concat("...")) - let err = Test.deployContract(name: name, path: spec.path, arguments: spec.arguments) - Test.expect(err, Test.beNil()) - Test.commitBlock() -} - -// UniV3 mainnet addresses — required by strategy contracts on init -access(all) let univ3Factory = "0xca6d7Bb03334bBf135902e1d919a5feccb461632" -access(all) let univ3Router = "0xeEDC6Ff75e1b10B903D9013c358e446a73d35341" -access(all) let univ3Quoter = "0x370A8DF17742867a44e56223EC20D82092242C85" - -access(all) fun setup() { - log("==== FlowYieldVaults Backward-Compatibility Redeploy Test ====") - - let contracts: [ContractSpec] = [ - ContractSpec( - path: "../../contracts/UInt64LinkedList.cdc", - arguments: [] - ), - ContractSpec( - path: "../../contracts/AutoBalancers.cdc", - arguments: [] - ), - ContractSpec( - path: "../../contracts/FlowYieldVaultsClosedBeta.cdc", - arguments: [] - ), - ContractSpec( - path: "../../contracts/FlowYieldVaultsSchedulerRegistryV1.cdc", - arguments: [] - ), - ContractSpec( - path: "../../contracts/FlowYieldVaults.cdc", - arguments: [] - ), - ContractSpec( - path: "../../contracts/FlowYieldVaultsAutoBalancers.cdc", - arguments: [] - ), - ContractSpec( - path: "../../contracts/FlowYieldVaultsAutoBalancersV1.cdc", - arguments: [] - ), - ContractSpec( - path: "../../contracts/FlowYieldVaultsSchedulerV1.cdc", - arguments: [] - ), - - // @TODO restore in strategies PR - // ContractSpec( - // path: "../../contracts/FlowYieldVaultsStrategiesV2.cdc", - // arguments: [univ3Factory, univ3Router, univ3Quoter] - // ), - ContractSpec( - path: "../../contracts/PMStrategiesV1.cdc", - arguments: [univ3Factory, univ3Router, univ3Quoter] - ) - ] - - for spec in contracts { - deployAndExpectSuccess(spec) - } - - log("==== All FlowYieldVaults contracts redeployed successfully ====") -} - -access(all) fun testAllContractsRedeployedWithoutError() { - log("All FlowYieldVaults contracts redeployed without error (verified in setup)") -} From e2e75d35c9ce69d8ab7e872e09b1073d89b3d28d Mon Sep 17 00:00:00 2001 From: Alex <12097569+nialexsan@users.noreply.github.com> Date: Tue, 31 Mar 2026 09:00:33 -0400 Subject: [PATCH 55/72] remove duplicate test --- cadence/tests/backcompat_redeploy_test.cdc | 397 --------------------- 1 file changed, 397 deletions(-) delete mode 100644 cadence/tests/backcompat_redeploy_test.cdc diff --git a/cadence/tests/backcompat_redeploy_test.cdc b/cadence/tests/backcompat_redeploy_test.cdc deleted file mode 100644 index a6920aa5..00000000 --- a/cadence/tests/backcompat_redeploy_test.cdc +++ /dev/null @@ -1,397 +0,0 @@ -#test_fork(network: "mainnet", height: nil) - -import Test - -import "EVM" -import "FlowToken" -import "FlowYieldVaults" -import "FlowYieldVaultsClosedBeta" -import "AutoBalancerCallbackWrapper" -import "FlowYieldVaultsAutoBalancers" -import "FlowYieldVaultsSchedulerRegistry" -import "FlowYieldVaultsSchedulerV1" -import "FlowYieldVaultsStrategiesV2" -import "PMStrategiesV1" - -/// Backward-compatibility fork test. -/// -/// Simulates a full contract upgrade by redeploying every contract in the -/// FlowYieldVaults ecosystem (and their dependencies) from the local codebase -/// on top of the live mainnet fork state. -/// -/// The test verifies that: -/// 1. All contracts can be redeployed without panicking. -/// 2. Pre-existing on-chain state (accounts, stored resources) is still -/// accessible after the upgrade. -/// 3. Core read-only operations (beta status, strategy list, vault IDs, -/// scheduler registry) continue to return sensible results. -/// 4. The admin StrategyComposerIssuer is still accessible and can issue -/// composers after the upgrade. -/// -/// Mainnet addresses: -/// Admin / deployer : 0xb1d63873c3cc9f79 -/// PYUSD0 test user : 0x443472749ebdaac8 -/// WBTC/WETH user : 0x68da18f20e98a7b6 - -// --- Accounts --- - -access(all) let adminAccount = Test.getAccount(0xb1d63873c3cc9f79) -access(all) let bandOracleAdmin = Test.getAccount(0x6801a6222ebf784a) -access(all) let pyusd0User = Test.getAccount(0x443472749ebdaac8) -access(all) let wbtcWethUser = Test.getAccount(0x68da18f20e98a7b6) - -// --- UniV3 addresses (mainnet) --- - -access(all) let univ3Factory = "0xca6d7Bb03334bBf135902e1d919a5feccb461632" -access(all) let univ3Router = "0xeEDC6Ff75e1b10B903D9013c358e446a73d35341" -access(all) let univ3Quoter = "0x370A8DF17742867a44e56223EC20D82092242C85" - -/* --- 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) -} - -/* --- Setup: redeploy every contract from local source --- */ - -access(all) fun setup() { - log("==== Backward-Compatibility Redeploy Test: Setup ====") - - // ------------------------------------------------------------------ // - // 1. FlowALP / FlowActions dependency contracts - // ------------------------------------------------------------------ // - - 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 ERC4626SinkConnectors...") - err = Test.deployContract( - name: "ERC4626SinkConnectors", - path: "../../lib/FlowALP/FlowActions/cadence/contracts/connectors/evm/ERC4626SinkConnectors.cdc", - arguments: [] - ) - Test.expect(err, Test.beNil()) - - 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 ERC4626PriceOracles...") - err = Test.deployContract( - name: "ERC4626PriceOracles", - path: "../../lib/FlowALP/FlowActions/cadence/contracts/connectors/evm/ERC4626PriceOracles.cdc", - arguments: [] - ) - Test.expect(err, Test.beNil()) - - // ------------------------------------------------------------------ // - // 2. Core FlowYieldVaults platform contracts - // (deployed at 0xb1d63873c3cc9f79 on mainnet) - // ------------------------------------------------------------------ // - - log("Deploying UInt64LinkedList...") - err = Test.deployContract( - name: "UInt64LinkedList", - path: "../../cadence/contracts/UInt64LinkedList.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 FlowYieldVaultsClosedBeta...") - err = Test.deployContract( - name: "FlowYieldVaultsClosedBeta", - path: "../../cadence/contracts/FlowYieldVaultsClosedBeta.cdc", - arguments: [] - ) - Test.expect(err, Test.beNil()) - - log("Deploying AutoBalancerCallbackWrapper...") - err = Test.deployContract( - name: "AutoBalancerCallbackWrapper", - path: "../../cadence/contracts/AutoBalancerCallbackWrapper.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 FlowYieldVaultsSchedulerRegistry...") - err = Test.deployContract( - name: "FlowYieldVaultsSchedulerRegistry", - path: "../../cadence/contracts/FlowYieldVaultsSchedulerRegistry.cdc", - arguments: [] - ) - Test.expect(err, Test.beNil()) - - log("Deploying FlowYieldVaultsSchedulerV1...") - err = Test.deployContract( - name: "FlowYieldVaultsSchedulerV1", - path: "../../cadence/contracts/FlowYieldVaultsSchedulerV1.cdc", - arguments: [] - ) - Test.expect(err, Test.beNil()) - - log("Deploying FlowYieldVaultsStrategiesV2...") - err = Test.deployContract( - name: "FlowYieldVaultsStrategiesV2", - path: "../../cadence/contracts/FlowYieldVaultsStrategiesV2.cdc", - arguments: [univ3Factory, univ3Router, univ3Quoter] - ) - Test.expect(err, Test.beNil()) - - log("Deploying PMStrategiesV1...") - err = Test.deployContract( - name: "PMStrategiesV1", - path: "../../cadence/contracts/PMStrategiesV1.cdc", - arguments: [univ3Factory, univ3Router, univ3Quoter] - ) - Test.expect(err, Test.beNil()) - - log("==== All contracts redeployed successfully ====") -} - -/* --- Tests --- */ - -/// All contracts deployed without error — already asserted in setup(). -/// This test serves as the initial smoke-check gate. -access(all) fun testAllContractsRedeployedWithoutError() { - log("All contracts redeployed without error (verified in setup)") -} - -/// Verify that FlowYieldVaults is accessible and returns supported strategies -/// after the upgrade. -access(all) fun testSupportedStrategiesReadable() { - log("Checking FlowYieldVaults.getSupportedStrategies() after redeploy...") - let result = _executeScript( - "../scripts/flow-yield-vaults/get_supported_strategies.cdc", - [] - ) - Test.expect(result, Test.beSucceeded()) - log("Supported strategies readable after redeploy") -} - -/// Verify that the SchedulerRegistry state is still readable after the upgrade. -access(all) fun testSchedulerRegistryReadable() { - log("Checking FlowYieldVaultsSchedulerRegistry state...") - let result = _executeScript( - "../scripts/flow-yield-vaults/get_registered_yield_vault_count.cdc", - [] - ) - Test.expect(result, Test.beSucceeded()) - let count = result.returnValue! as! Int - log("Registered yield vault count after redeploy: ".concat(count.toString())) -} - -/// Verify that the pending vault queue is still intact after the upgrade. -access(all) fun testPendingQueueReadable() { - log("Checking pending yield vault queue...") - let result = _executeScript( - "../scripts/flow-yield-vaults/get_pending_count.cdc", - [] - ) - Test.expect(result, Test.beSucceeded()) - let count = result.returnValue! as! Int - log("Pending vault count after redeploy: ".concat(count.toString())) -} - -/// Verify that existing yield vault IDs for the PYUSD0 user are still readable. -access(all) fun testExistingPyusd0UserVaultsReadable() { - log("Checking PYUSD0 user yield vault IDs after redeploy...") - let result = _executeScript( - "../scripts/flow-yield-vaults/get_yield_vault_ids.cdc", - [pyusd0User.address] - ) - Test.expect(result, Test.beSucceeded()) - let ids = result.returnValue! as! [UInt64]? - if ids == nil || ids!.length == 0 { - log("PYUSD0 user has no existing yield vaults on mainnet (OK for closed beta)") - } else { - log("PYUSD0 user existing vault IDs: ".concat(ids!.length.toString()).concat(" vaults found")) - // Verify balance is readable for each vault - for id in ids! { - let balResult = _executeScript( - "../scripts/flow-yield-vaults/get_yield_vault_balance.cdc", - [pyusd0User.address, id] - ) - Test.expect(balResult, Test.beSucceeded()) - log(" Vault ".concat(id.toString()).concat(" balance readable")) - } - } -} - -/// Verify that existing yield vault IDs for the WBTC/WETH user are still readable. -access(all) fun testExistingWbtcWethUserVaultsReadable() { - log("Checking WBTC/WETH user yield vault IDs after redeploy...") - let result = _executeScript( - "../scripts/flow-yield-vaults/get_yield_vault_ids.cdc", - [wbtcWethUser.address] - ) - Test.expect(result, Test.beSucceeded()) - let ids = result.returnValue! as! [UInt64]? - if ids == nil || ids!.length == 0 { - log("WBTC/WETH user has no existing yield vaults on mainnet (OK for closed beta)") - } else { - log("WBTC/WETH user existing vault IDs: ".concat(ids!.length.toString()).concat(" vaults found")) - for id in ids! { - let balResult = _executeScript( - "../scripts/flow-yield-vaults/get_yield_vault_balance.cdc", - [wbtcWethUser.address, id] - ) - Test.expect(balResult, Test.beSucceeded()) - log(" Vault ".concat(id.toString()).concat(" balance readable")) - } - } -} - -/// Verify that existing yield vault IDs for the admin account are still readable. -access(all) fun testExistingAdminVaultsReadable() { - log("Checking admin yield vault IDs after redeploy...") - let result = _executeScript( - "../scripts/flow-yield-vaults/get_yield_vault_ids.cdc", - [adminAccount.address] - ) - Test.expect(result, Test.beSucceeded()) - let ids = result.returnValue! as! [UInt64]? - if ids == nil || ids!.length == 0 { - log("Admin has no existing yield vaults (OK)") - } else { - log("Admin existing vault IDs: ".concat(ids!.length.toString()).concat(" vaults found")) - for id in ids! { - let balResult = _executeScript( - "../scripts/flow-yield-vaults/get_yield_vault_balance.cdc", - [adminAccount.address, id] - ) - Test.expect(balResult, Test.beSucceeded()) - log(" Vault ".concat(id.toString()).concat(" balance readable")) - } - } -} - -/// Verify that the closed beta gate still functions correctly after the upgrade. -access(all) fun testClosedBetaStatePreserved() { - log("Checking closed beta state after redeploy...") - let betaCapResult = _executeScript( - "../scripts/flow-yield-vaults/get_beta_cap.cdc", - [adminAccount.address] - ) - Test.expect(betaCapResult, Test.beSucceeded()) - log("Beta cap accessible after redeploy") - - let activeCountResult = _executeScript( - "../scripts/flow-yield-vaults/get_active_beta_count.cdc", - [] - ) - Test.expect(activeCountResult, Test.beSucceeded()) - let count = activeCountResult.returnValue! as! Int - log("Active beta user count after redeploy: ".concat(count.toString())) -} - -/// Verify that the PMStrategiesV1 IssuerStoragePath is still accessible after -/// the upgrade and that strategy configuration can be upserted. -access(all) fun testPMStrategiesV1IssuerAccessible() { - log("Configuring PMStrategiesV1 syWFLOWv strategy for FLOW collateral...") - let result = _executeTransactionFile( - "../transactions/flow-yield-vaults/admin/upsert-pm-strategy-config.cdc", - [ - "A.b1d63873c3cc9f79.PMStrategiesV1.syWFLOWvStrategy", - "A.1654653399040a61.FlowToken.Vault", - "0xCBf9a7753F9D2d0e8141ebB36d99f87AcEf98597", // syWFLOWv - UInt32(100) // WFLOW/syWFLOWv fee tier - ], - [adminAccount] - ) - Test.expect(result, Test.beSucceeded()) - log("PMStrategiesV1 syWFLOWv strategy config upserted successfully") -} - -/// Verify that FlowYieldVaultsStrategiesV2 StrategyComposerIssuer is accessible -/// and can be configured with a new strategy after the upgrade. -access(all) fun testStrategiesV2IssuerConfigurable() { - log("Configuring FlowYieldVaultsStrategiesV2 FUSDEVStrategy for PYUSD0 collateral...") - // PYUSD0 collateral: yield -> collateral path is [FUSDEV_EVM, PYUSD0_EVM], fee 100 - let result = _executeTransactionFile( - "../transactions/flow-yield-vaults/admin/upsert_strategy_config.cdc", - [ - "A.b1d63873c3cc9f79.FlowYieldVaultsStrategiesV2.FUSDEVStrategy", - "A.1e4aa0b87d10b141.EVMVMBridgedToken_99af3eea856556646c98c8b9b2548fe815240750.Vault", - "0xd069d989e2F44B70c65347d1853C0c67e10a9F8D", // FUSDEV yield token - ["0xd069d989e2F44B70c65347d1853C0c67e10a9F8D", - "0x99aF3EeA856556646C98c8B9b2548Fe815240750"], // yield -> PYUSD0 - [UInt32(100)] - ], - [adminAccount] - ) - Test.expect(result, Test.beSucceeded()) - log("FlowYieldVaultsStrategiesV2 FUSDEVStrategy config upserted successfully") -} From e295f5a2d44b4a89560e6e80e9a3de28943086f4 Mon Sep 17 00:00:00 2001 From: Alex <12097569+nialexsan@users.noreply.github.com> Date: Tue, 31 Mar 2026 09:22:34 -0400 Subject: [PATCH 56/72] resove merge conflict --- .../contracts/FlowYieldVaultsStrategiesV2.cdc | 389 +----------------- .../admin/upsert_moet_preswap_config.cdc | 49 --- 2 files changed, 1 insertion(+), 437 deletions(-) delete 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 93f750e1..7a6320d5 100644 --- a/cadence/contracts/FlowYieldVaultsStrategiesV2.cdc +++ b/cadence/contracts/FlowYieldVaultsStrategiesV2.cdc @@ -45,15 +45,11 @@ access(all) contract FlowYieldVaultsStrategiesV2 { /// Partitioned config map. Each key is a partition name; each value is a typed nested map keyed by /// strategy UniqueIdentifier ID (UInt64). Current partitions: /// "closedPositions" → {UInt64: Bool} -<<<<<<< HEAD /// "syWFLOWvDebtTokenTypes" → {UInt64: Type} /// "moreERC4626Configs" → {Type: {Type: {Type: MoreERC4626CollateralConfig}}} // @deprecated - should be used only for working with legacy positions - /// "moetPreswapConfigs" → {Type: {Type: MoetPreswapConfig}} /// "originalCollateralTypes" → {UInt64: Type} -======= ->>>>>>> origin/v0 access(contract) let config: {String: AnyStruct} /// Canonical StoragePath where the StrategyComposerIssuer should be stored @@ -126,6 +122,7 @@ access(all) contract FlowYieldVaultsStrategiesV2 { } } + // @deprecated /// 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). @@ -315,11 +312,7 @@ access(all) contract FlowYieldVaultsStrategiesV2 { let yieldTokenSource = FlowYieldVaultsAutoBalancersV1.createExternalSource(id: self.id()!) ?? panic("Could not create external source from AutoBalancer") -<<<<<<< HEAD // Step 5: Reconstruct yield→PYUSD0 swapper from stored CollateralConfig. -======= - // Step 5: Reconstruct yield→MOET swapper from stored CollateralConfig. ->>>>>>> origin/v0 let closeCollateralConfig = self._getStoredCollateralConfig( strategyType: Type<@FUSDEVStrategy>(), collateralType: collateralType @@ -327,18 +320,13 @@ access(all) contract FlowYieldVaultsStrategiesV2 { let closeTokens = FlowYieldVaultsStrategiesV2._resolveTokenBundle( yieldTokenEVMAddress: closeCollateralConfig.yieldTokenEVMAddress ) -<<<<<<< HEAD let yieldToPyusd0Swapper = self._buildYieldToDebtSwapper( -======= - let yieldToMoetSwapper = self._buildYieldToDebtSwapper( ->>>>>>> origin/v0 tokens: closeTokens, uniqueID: self.uniqueID! ) // Step 6: Pre-supplement from collateral if yield is insufficient to cover the full debt. // -<<<<<<< HEAD // The FUSDEV close path has a negligible round-trip fee: // Open: PYUSD0 → FUSDEV (ERC4626 deposit, free) // Close: FUSDEV → PYUSD0 (ERC4626 redeem, free) @@ -347,40 +335,21 @@ access(all) contract FlowYieldVaultsStrategiesV2 { // less PYUSD0 than was borrowed. We handle this by pre-pulling a tiny amount of // collateral from self.source, swapping it to PYUSD0, and depositing it into the // position to reduce the outstanding debt — BEFORE calling position.closePosition. -======= - // 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. ->>>>>>> origin/v0 // // 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() -<<<<<<< HEAD let expectedPyusd0 = yieldAvail > 0.0 ? yieldToPyusd0Swapper.quoteOut(forProvided: yieldAvail, reverse: false).outAmount : 0.0 if expectedPyusd0 < totalDebtAmount { let collateralToPyusd0Swapper = self._buildCollateralToDebtSwapper( -======= - let expectedMOET = yieldAvail > 0.0 - ? yieldToMoetSwapper.quoteOut(forProvided: yieldAvail, reverse: false).outAmount - : 0.0 - if expectedMOET < totalDebtAmount { - let collateralToMoetSwapper = self._buildCollateralToDebtSwapper( ->>>>>>> origin/v0 collateralConfig: closeCollateralConfig, tokens: closeTokens, collateralType: collateralType, uniqueID: self.uniqueID! ) -<<<<<<< HEAD let shortfall = totalDebtAmount - expectedPyusd0 let quote = collateralToPyusd0Swapper.quoteIn(forDesired: shortfall, reverse: false) assert(quote.inAmount > 0.0, @@ -399,31 +368,10 @@ access(all) contract FlowYieldVaultsStrategiesV2 { // use quoteIn(remainingDebt) and pull only the shares needed — not the full balance. let debtSource = SwapConnectors.SwapSource( swapper: yieldToPyusd0Swapper, -======= - let shortfall = totalDebtAmount - expectedMOET - let quote = collateralToMoetSwapper.quoteIn(forDesired: shortfall, reverse: false) - assert(quote.inAmount > 0.0, - message: "Pre-supplement: collateral→MOET quote returned zero input for non-zero shortfall — swapper misconfigured") - let extraCollateral <- self.source.withdrawAvailable(maxAmount: quote.inAmount) - assert(extraCollateral.balance > 0.0, - message: "Pre-supplement: no collateral available to cover shortfall of \(shortfall) MOET") - let extraMOET <- collateralToMoetSwapper.swap(quote: quote, inVault: <-extraCollateral) - assert(extraMOET.balance >= shortfall, - message: "Pre-supplement: collateral→MOET swap produced less than shortfall: got \(extraMOET.balance), need \(shortfall)") - self.position.deposit(from: <-extraMOET) - } - - // Step 7: Create a SwapSource that converts yield tokens → MOET for debt repayment. - // Step 6's pre-supplement ensures remaining debt ≤ yield value, so SwapSource will - // use quoteIn(remainingDebt) and pull only the shares needed — not the full balance. - let moetSource = SwapConnectors.SwapSource( - swapper: yieldToMoetSwapper, ->>>>>>> origin/v0 source: yieldTokenSource, uniqueID: self.copyID() ) -<<<<<<< HEAD // Step 8: Close position - pool pulls up to the (now pre-reduced) debt from debtSource let resultVaults <- self.position.closePosition(repaymentSources: [debtSource]) @@ -433,17 +381,6 @@ access(all) contract FlowYieldVaultsStrategiesV2 { // assume the collateral vault is first. Find it by type and convert any non-collateral // vaults (PYUSD0 overpayment dust) back to collateral via reconstructed swapper. // Reconstruct PYUSD0→collateral path from CollateralConfig. -======= - // Step 8: Close position - pool pulls up to the (now pre-reduced) debt from moetSource - let resultVaults <- self.position.closePosition(repaymentSources: [moetSource]) - - // With one collateral type and one debt type, the pool returns at most two vaults: - // the collateral vault and optionally a MOET overpayment dust vault. - // closePosition returns vaults in dict-iteration order (hash-based), so we cannot - // assume the collateral vault is first. Find it by type and convert any non-collateral - // vaults (MOET overpayment dust) back to collateral via reconstructed swapper. - // Reconstruct MOET→YIELD→collateral from CollateralConfig. ->>>>>>> origin/v0 let debtToCollateralSwapper = self._buildDebtToCollateralSwapper( collateralConfig: closeCollateralConfig, tokens: closeTokens, @@ -488,7 +425,6 @@ access(all) contract FlowYieldVaultsStrategiesV2 { // any surplus shares are still held by the AutoBalancer and are recovered here. let excessShares <- yieldTokenSource.withdrawAvailable(maxAmount: UFix64.max) if excessShares.balance > 0.0 { -<<<<<<< HEAD let sharesToCollateral = SwapConnectors.SequentialSwapper( swappers: [yieldToPyusd0Swapper, debtToCollateralSwapper], uniqueID: self.copyID() @@ -497,44 +433,15 @@ access(all) contract FlowYieldVaultsStrategiesV2 { if quote.outAmount > 0.0 { let extraCollateral <- sharesToCollateral.swap(quote: quote, inVault: <-excessShares) collateralVault.deposit(from: <-extraCollateral) -======= - let moetQuote = yieldToMoetSwapper.quoteOut(forProvided: excessShares.balance, reverse: false) - if moetQuote.outAmount > 0.0 { - let moetVault <- yieldToMoetSwapper.swap(quote: moetQuote, inVault: <-excessShares) - let collQuote = debtToCollateralSwapper.quoteOut(forProvided: moetVault.balance, reverse: false) - if collQuote.outAmount > 0.0 { - let extraCollateral <- debtToCollateralSwapper.swap(quote: collQuote, inVault: <-moetVault) - collateralVault.deposit(from: <-extraCollateral) - } else { - emit DustBurned( - tokenType: moetVault.getType().identifier, - balance: moetVault.balance, - quoteInType: collQuote.inType.identifier, - quoteOutType: collQuote.outType.identifier, - quoteInAmount: collQuote.inAmount, - quoteOutAmount: collQuote.outAmount, - swapperType: debtToCollateralSwapper.getType().identifier - ) - Burner.burn(<-moetVault) - } ->>>>>>> origin/v0 } else { emit DustBurned( tokenType: excessShares.getType().identifier, balance: excessShares.balance, -<<<<<<< HEAD quoteInType: quote.inType.identifier, quoteOutType: quote.outType.identifier, quoteInAmount: quote.inAmount, quoteOutAmount: quote.outAmount, swapperType: sharesToCollateral.getType().identifier -======= - quoteInType: moetQuote.inType.identifier, - quoteOutType: moetQuote.outType.identifier, - quoteInAmount: moetQuote.inAmount, - quoteOutAmount: moetQuote.outAmount, - swapperType: yieldToMoetSwapper.getType().identifier ->>>>>>> origin/v0 ) Burner.burn(<-excessShares) } @@ -582,19 +489,12 @@ access(all) contract FlowYieldVaultsStrategiesV2 { return issuer!.getCollateralConfig(strategyType: strategyType, collateralType: collateralType) } -<<<<<<< HEAD /// Builds a YIELD→PYUSD0 MultiSwapper (AMM direct + ERC4626 redeem path). /// PYUSD0 is the underlying asset of the FUSDEV vault and is also the debt token. -======= - /// Builds a YIELD→MOET MultiSwapper that contains 2 paths (AMM direct + ERC4626 redeem path). - // AMM direct path: YIELD (FUSDEV) -> MOET - // ERC4626 redeem path: YIELD (FUSDEV) -> PYUSD0 -> MOET ->>>>>>> origin/v0 access(self) fun _buildYieldToDebtSwapper( tokens: FlowYieldVaultsStrategiesV2.TokenBundle, uniqueID: DeFiActions.UniqueIdentifier ): SwapConnectors.MultiSwapper { -<<<<<<< HEAD // Direct FUSDEV→PYUSD0 via AMM (fee 100) let yieldToDebtAMM = FlowYieldVaultsStrategiesV2._buildUniV3Swapper( tokenPath: [tokens.yieldTokenEVMAddress, tokens.underlying4626AssetEVMAddress], @@ -604,15 +504,6 @@ access(all) contract FlowYieldVaultsStrategiesV2 { uniqueID: uniqueID ) // FUSDEV→PYUSD0 via Morpho ERC4626 redeem (no additional swap needed) -======= - let yieldToDebtAMM = FlowYieldVaultsStrategiesV2._buildUniV3Swapper( - tokenPath: [tokens.yieldTokenEVMAddress, tokens.moetTokenEVMAddress], - feePath: [100], - inVault: tokens.yieldTokenType, - outVault: tokens.moetTokenType, - uniqueID: uniqueID - ) ->>>>>>> origin/v0 let yieldToUnderlying = MorphoERC4626SwapConnectors.Swapper( vaultEVMAddress: tokens.yieldTokenEVMAddress, coa: FlowYieldVaultsStrategiesV2._getCOACapability(), @@ -620,43 +511,18 @@ access(all) contract FlowYieldVaultsStrategiesV2 { uniqueID: uniqueID, isReversed: true ) -<<<<<<< HEAD return SwapConnectors.MultiSwapper( inVault: tokens.yieldTokenType, outVault: tokens.underlying4626AssetType, swappers: [yieldToDebtAMM, yieldToUnderlying], -======= - let underlyingToDebt = FlowYieldVaultsStrategiesV2._buildUniV3Swapper( - tokenPath: [tokens.underlying4626AssetEVMAddress, tokens.moetTokenEVMAddress], - feePath: [100], - inVault: tokens.underlying4626AssetType, - outVault: tokens.moetTokenType, - uniqueID: uniqueID - ) - let seq = SwapConnectors.SequentialSwapper( - swappers: [yieldToUnderlying, underlyingToDebt], - uniqueID: uniqueID - ) - return SwapConnectors.MultiSwapper( - inVault: tokens.yieldTokenType, - outVault: tokens.moetTokenType, - swappers: [yieldToDebtAMM, seq], ->>>>>>> origin/v0 uniqueID: uniqueID ) } -<<<<<<< HEAD /// Builds a collateral→PYUSD0 UniV3 swapper from CollateralConfig. /// Derives the path by reversing yieldToCollateralUniV3AddressPath[1..] (skipping the /// yield token); PYUSD0 is the underlying asset and the debt token, so no further hop needed. /// e.g. [FUSDEV, PYUSD0, WETH, WBTC] → [WBTC, WETH, PYUSD0] -======= - /// Builds a collateral→MOET UniV3 swapper from CollateralConfig. - /// Derives the path by reversing yieldToCollateralUniV3AddressPath[1..] (skipping the - /// yield token) and appending MOET, preserving all intermediate hops. - /// e.g. [FUSDEV, PYUSD0, WETH, WBTC] → [WBTC, WETH, PYUSD0, MOET] ->>>>>>> origin/v0 access(self) fun _buildCollateralToDebtSwapper( collateralConfig: FlowYieldVaultsStrategiesV2.CollateralConfig, tokens: FlowYieldVaultsStrategiesV2.TokenBundle, @@ -665,62 +531,37 @@ access(all) contract FlowYieldVaultsStrategiesV2 { ): UniswapV3SwapConnectors.Swapper { let yieldToCollPath = collateralConfig.yieldToCollateralUniV3AddressPath let yieldToCollFees = collateralConfig.yieldToCollateralUniV3FeePath -<<<<<<< HEAD assert(yieldToCollPath.length >= 2, message: "yieldToCollateral path must have at least 2 elements") // Build reversed path: iterate yieldToCollPath from last down to index 1 (skip yield token at 0). // e.g. [FUSDEV, PYUSD0, WETH, WBTC] → [WBTC, WETH, PYUSD0] -======= - assert(yieldToCollPath.length >= 2, message: "yieldToCollateral path requires at least yield and collateral tokens, got \(yieldToCollPath.length)") - // Build reversed path: iterate yieldToCollPath from last down to index 1 (skip yield token at 0), - // then append MOET. e.g. [FUSDEV, PYUSD0, WETH, WBTC] → [WBTC, WETH, PYUSD0] + MOET ->>>>>>> origin/v0 var collToDebtPath: [EVM.EVMAddress] = [] var collToDebtFees: [UInt32] = [] for i in InclusiveRange(yieldToCollPath.length - 1, 1, step: -1) { collToDebtPath.append(yieldToCollPath[i]) } -<<<<<<< HEAD // Build reversed fees: iterate from last down to index 1 (skip yield→underlying fee at 0). // e.g. [100, 3000, 3000] → [3000, 3000] for i in InclusiveRange(yieldToCollFees.length - 1, 1, step: -1) { collToDebtFees.append(yieldToCollFees[i]) } -======= - collToDebtPath.append(tokens.moetTokenEVMAddress) - // Build reversed fees: iterate from last down to index 1 (skip yield→underlying fee at 0), - // then append PYUSD0→MOET fee (100). e.g. [100, 3000, 3000] → [3000, 3000] + 100 - for i in InclusiveRange(yieldToCollFees.length - 1, 1, step: -1) { - collToDebtFees.append(yieldToCollFees[i]) - } - collToDebtFees.append(UInt32(100)) ->>>>>>> origin/v0 return FlowYieldVaultsStrategiesV2._buildUniV3Swapper( tokenPath: collToDebtPath, feePath: collToDebtFees, inVault: collateralType, -<<<<<<< HEAD outVault: tokens.underlying4626AssetType, -======= - outVault: tokens.moetTokenType, ->>>>>>> origin/v0 uniqueID: uniqueID ) } -<<<<<<< HEAD /// Builds a PYUSD0→collateral UniV3 swapper for overpayment dust handling. /// Uses the yieldToCollateral path[1..] (skipping the yield token at index 0), /// going directly from PYUSD0 (the debt/underlying token) to collateral. /// e.g. [FUSDEV, PYUSD0, WETH] fees [100, 3000] → [PYUSD0, WETH] fees [3000] -======= - /// Builds a MOET→collateral SequentialSwapper for dust handling: MOET→YIELD→collateral. ->>>>>>> origin/v0 access(self) fun _buildDebtToCollateralSwapper( collateralConfig: FlowYieldVaultsStrategiesV2.CollateralConfig, tokens: FlowYieldVaultsStrategiesV2.TokenBundle, collateralType: Type, uniqueID: DeFiActions.UniqueIdentifier -<<<<<<< HEAD ): UniswapV3SwapConnectors.Swapper { let path = collateralConfig.yieldToCollateralUniV3AddressPath let fees = collateralConfig.yieldToCollateralUniV3FeePath @@ -741,27 +582,6 @@ access(all) contract FlowYieldVaultsStrategiesV2 { outVault: collateralType, uniqueID: uniqueID ) -======= - ): SwapConnectors.SequentialSwapper { - let debtToYieldAMM = FlowYieldVaultsStrategiesV2._buildUniV3Swapper( - tokenPath: [tokens.moetTokenEVMAddress, tokens.yieldTokenEVMAddress], - feePath: [100], - inVault: tokens.moetTokenType, - outVault: tokens.yieldTokenType, - uniqueID: uniqueID - ) - let yieldToCollateral = FlowYieldVaultsStrategiesV2._buildUniV3Swapper( - tokenPath: collateralConfig.yieldToCollateralUniV3AddressPath, - feePath: collateralConfig.yieldToCollateralUniV3FeePath, - inVault: tokens.yieldTokenType, - outVault: collateralType, - uniqueID: uniqueID - ) - return SwapConnectors.SequentialSwapper( - swappers: [debtToYieldAMM, yieldToCollateral], - uniqueID: uniqueID - ) ->>>>>>> origin/v0 } access(self) view fun _isPositionClosed(): Bool { @@ -787,7 +607,6 @@ access(all) contract FlowYieldVaultsStrategiesV2 { FlowYieldVaultsStrategiesV2.config["closedPositions"] = partition } } -<<<<<<< HEAD } /// This strategy uses syWFLOWv vault (More ERC4626). @@ -1094,8 +913,6 @@ access(all) contract FlowYieldVaultsStrategiesV2 { uniqueID: uniqueID ) } -======= ->>>>>>> origin/v0 } access(all) struct TokenBundle { @@ -1182,12 +999,8 @@ access(all) contract FlowYieldVaultsStrategiesV2 { } /// Resolves the full token bundle for a strategy given the ERC4626 yield vault address. -<<<<<<< HEAD /// moetTokenType/moetTokenEVMAddress are retained in the struct for upgrade compatibility /// but are no longer used by active strategy code. -======= - /// The MOET token is always the pool's default token. ->>>>>>> origin/v0 access(self) fun _resolveTokenBundle(yieldTokenEVMAddress: EVM.EVMAddress): FlowYieldVaultsStrategiesV2.TokenBundle { let moetTokenType = FlowYieldVaultsStrategiesV2._getPoolDefaultToken() let moetTokenEVMAddress = FlowEVMBridgeConfig.getEVMAddressAssociated(with: moetTokenType) @@ -1362,17 +1175,10 @@ access(all) contract FlowYieldVaultsStrategiesV2 { switch type { // ----------------------------------------------------------------------- -<<<<<<< HEAD // FUSDEVStrategy: borrows PYUSD0 from the FlowALP position, deposits into FUSDEV // ----------------------------------------------------------------------- case Type<@FUSDEVStrategy>(): // Swappers: PYUSD0 (underlying/debt) <-> YIELD (FUSDEV) -======= - // FUSDEVStrategy: borrows MOET from the FlowALP position, swaps to FUSDEV - // ----------------------------------------------------------------------- - case Type<@FUSDEVStrategy>(): - // Swappers: MOET <-> YIELD ->>>>>>> origin/v0 let debtToYieldSwapper = self._createDebtToYieldSwapper(tokens: tokens, uniqueID: uniqueID) let yieldToDebtSwapper = self._createYieldToDebtSwapper(tokens: tokens, uniqueID: uniqueID) @@ -1491,22 +1297,15 @@ access(all) contract FlowYieldVaultsStrategiesV2 { tokens: FlowYieldVaultsStrategiesV2.TokenBundle, uniqueID: DeFiActions.UniqueIdentifier ): SwapConnectors.MultiSwapper { -<<<<<<< HEAD // Direct PYUSD0 → FUSDEV via AMM (fee 100) let debtToYieldAMM = self._createUniV3Swapper( tokenPath: [tokens.underlying4626AssetEVMAddress, tokens.yieldTokenEVMAddress], -======= - // Direct MOET -> YIELD via AMM - let debtToYieldAMM = self._createUniV3Swapper( - tokenPath: [tokens.moetTokenEVMAddress, tokens.yieldTokenEVMAddress], ->>>>>>> origin/v0 feePath: [100], inVault: tokens.underlying4626AssetType, outVault: tokens.yieldTokenType, uniqueID: uniqueID ) -<<<<<<< HEAD // PYUSD0 → FUSDEV via Morpho ERC4626 vault deposit (no AMM swap needed) let underlyingTo4626 = MorphoERC4626SwapConnectors.Swapper( vaultEVMAddress: tokens.yieldTokenEVMAddress, @@ -1531,96 +1330,27 @@ access(all) contract FlowYieldVaultsStrategiesV2 { // Direct FUSDEV → PYUSD0 via AMM (fee 100) let yieldToDebtAMM = self._createUniV3Swapper( tokenPath: [tokens.yieldTokenEVMAddress, tokens.underlying4626AssetEVMAddress], -======= - // MOET -> UNDERLYING via AMM - let debtToUnderlying = self._createUniV3Swapper( - tokenPath: [tokens.moetTokenEVMAddress, tokens.underlying4626AssetEVMAddress], ->>>>>>> origin/v0 feePath: [100], inVault: tokens.yieldTokenType, outVault: tokens.underlying4626AssetType, uniqueID: uniqueID ) -<<<<<<< HEAD // FUSDEV → PYUSD0 via Morpho ERC4626 redeem (no additional AMM swap needed) let yieldToUnderlying = MorphoERC4626SwapConnectors.Swapper( -======= - // UNDERLYING -> YIELD via Morpho ERC4626 vault deposit - let underlyingTo4626 = MorphoERC4626SwapConnectors.Swapper( ->>>>>>> origin/v0 vaultEVMAddress: tokens.yieldTokenEVMAddress, coa: FlowYieldVaultsStrategiesV2._getCOACapability(), feeSource: FlowYieldVaultsStrategiesV2._createFeeSource(withID: uniqueID), uniqueID: uniqueID, -<<<<<<< HEAD isReversed: true ) return SwapConnectors.MultiSwapper( -======= - isReversed: false - ) - - let seq = SwapConnectors.SequentialSwapper( - swappers: [debtToUnderlying, underlyingTo4626], - uniqueID: uniqueID - ) - - return SwapConnectors.MultiSwapper( - inVault: tokens.moetTokenType, - outVault: tokens.yieldTokenType, - swappers: [debtToYieldAMM, seq], - uniqueID: uniqueID - ) - } - - access(self) fun _createYieldToDebtSwapper( - tokens: FlowYieldVaultsStrategiesV2.TokenBundle, - uniqueID: DeFiActions.UniqueIdentifier - ): SwapConnectors.MultiSwapper { - // Direct YIELD -> MOET via AMM - let yieldToDebtAMM = self._createUniV3Swapper( - tokenPath: [tokens.yieldTokenEVMAddress, tokens.moetTokenEVMAddress], - feePath: [100], ->>>>>>> origin/v0 inVault: tokens.yieldTokenType, outVault: tokens.underlying4626AssetType, swappers: [yieldToDebtAMM, yieldToUnderlying], uniqueID: uniqueID ) -<<<<<<< HEAD -======= - - // 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 - ) - - return SwapConnectors.MultiSwapper( - inVault: tokens.yieldTokenType, - outVault: tokens.moetTokenType, - swappers: [yieldToDebtAMM, seq], - uniqueID: uniqueID - ) ->>>>>>> origin/v0 } access(self) fun _createYieldToCollateralSwapper( @@ -1686,22 +1416,12 @@ access(all) contract FlowYieldVaultsStrategiesV2 { ) } -<<<<<<< HEAD /// Creates a Collateral → Debt (PYUSD0) swapper using UniswapV3. /// Path: collateral → underlying (PYUSD0) /// /// The fee for collateral→underlying is the last fee in yieldToCollateral (reversed). /// Used by FUSDEVStrategy.closePosition to pre-reduce position debt from /// collateral when yield tokens alone cannot cover the full outstanding PYUSD0 debt. -======= - /// 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. ->>>>>>> origin/v0 /// access(self) fun _createCollateralToDebtSwapper( collateralConfig: FlowYieldVaultsStrategiesV2.CollateralConfig, @@ -1713,7 +1433,6 @@ access(all) contract FlowYieldVaultsStrategiesV2 { let yieldToCollFees = collateralConfig.yieldToCollateralUniV3FeePath // collateral EVM address = last element of yieldToCollateral path -<<<<<<< HEAD // underlying (PYUSD0) EVM address = second element of yieldToCollateral path (index 1) assert(yieldToCollPath.length >= 2, message: "yieldToCollateral path must have at least 2 elements") let collateralEVMAddress = yieldToCollPath[yieldToCollPath.length - 1] @@ -1899,26 +1618,6 @@ access(all) contract FlowYieldVaultsStrategiesV2 { panic("Unsupported strategy type \(type.identifier)") } } -======= - // underlying (PYUSD0) EVM address = second element of yieldToCollateral path - assert(yieldToCollPath.length >= 2, message: "yieldToCollateral path requires at least yield and collateral tokens, got \(yieldToCollPath.length)") - 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 - ) - } ->>>>>>> origin/v0 } access(all) entitlement Configure @@ -1945,7 +1644,6 @@ access(all) contract FlowYieldVaultsStrategiesV2 { ) } -<<<<<<< HEAD access(self) fun makeMoreERC4626CollateralConfig( yieldTokenEVMAddress: EVM.EVMAddress, @@ -1977,8 +1675,6 @@ access(all) contract FlowYieldVaultsStrategiesV2 { ) } -======= ->>>>>>> origin/v0 /// 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 @@ -2042,13 +1738,10 @@ access(all) contract FlowYieldVaultsStrategiesV2 { return <- create MorphoERC4626StrategyComposer( self.configs[type] ?? panic("No config registered for \(type.identifier)") ) -<<<<<<< HEAD case Type<@MoreERC4626StrategyComposer>(): let moreCfg = FlowYieldVaultsStrategiesV2._getMoreERC4626ComposerConfig(type) assert(moreCfg.length > 0, message: "No config registered for \(type.identifier)") return <- create MoreERC4626StrategyComposer(moreCfg) -======= ->>>>>>> origin/v0 default: panic("Unsupported StrategyComposer \(type.identifier) requested") } @@ -2081,7 +1774,6 @@ access(all) contract FlowYieldVaultsStrategiesV2 { self.configs[composerType] = composerPartition } -<<<<<<< HEAD /// Merges new MoreERC4626CollateralConfig entries into the MoreERC4626StrategyComposer config. access(Configure) fun upsertMoreERC4626Config( @@ -2110,8 +1802,6 @@ access(all) contract FlowYieldVaultsStrategiesV2 { } } -======= ->>>>>>> origin/v0 access(Configure) fun addOrUpdateMorphoCollateralConfig( strategyType: Type, collateralVaultType: Type, @@ -2132,7 +1822,6 @@ access(all) contract FlowYieldVaultsStrategiesV2 { yieldToCollateralFeePath: yieldToCollateralFeePath ) self.upsertMorphoConfig(config: { strategyType: { collateralVaultType: base } }) -<<<<<<< HEAD } access(Configure) fun addOrUpdateMoreERC4626CollateralConfig( @@ -2161,43 +1850,6 @@ 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 - ) - ) - } - -======= - } - ->>>>>>> origin/v0 access(Configure) fun purgeConfig() { self.configs = { Type<@MorphoERC4626StrategyComposer>(): { @@ -2205,7 +1857,6 @@ access(all) contract FlowYieldVaultsStrategiesV2 { } } FlowYieldVaultsStrategiesV2._purgeMoreERC4626Configs() - FlowYieldVaultsStrategiesV2.config["moetPreswapConfigs"] = {} as {Type: {Type: FlowYieldVaultsStrategiesV2.MoetPreswapConfig}} } } @@ -2295,7 +1946,6 @@ access(all) contract FlowYieldVaultsStrategiesV2 { coaCapability: FlowYieldVaultsStrategiesV2._getCOACapability(), uniqueID: uniqueID ) -<<<<<<< HEAD } // --- "syWFLOWvDebtTokenTypes" partition --- @@ -2389,35 +2039,6 @@ access(all) contract FlowYieldVaultsStrategiesV2 { FlowYieldVaultsStrategiesV2.config["originalCollateralTypes"] = 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 -======= ->>>>>>> origin/v0 - } - init( univ3FactoryEVMAddress: String, univ3RouterEVMAddress: String, @@ -2429,14 +2050,6 @@ access(all) contract FlowYieldVaultsStrategiesV2 { self.IssuerStoragePath = StoragePath(identifier: "FlowYieldVaultsStrategyV2ComposerIssuer_\(self.account.address)")! self.config = {} -<<<<<<< HEAD -======= - let moetType = Type<@MOET.Vault>() - if FlowEVMBridgeConfig.getEVMAddressAssociated(with: Type<@MOET.Vault>()) == nil { - panic("Could not find EVM address for \(moetType.identifier) - ensure the asset is onboarded to the VM Bridge") - } - ->>>>>>> origin/v0 let issuer <- create StrategyComposerIssuer( configs: { Type<@MorphoERC4626StrategyComposer>(): { 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 deleted file mode 100644 index 858ac696..00000000 --- a/cadence/transactions/flow-yield-vaults/admin/upsert_moet_preswap_config.cdc +++ /dev/null @@ -1,49 +0,0 @@ -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 b130bb2de3a279d9b45fbc180394cda9a2c0b476 Mon Sep 17 00:00:00 2001 From: Alex <12097569+nialexsan@users.noreply.github.com> Date: Tue, 31 Mar 2026 09:26:14 -0400 Subject: [PATCH 57/72] remove unused originalCollateralType --- .../contracts/FlowYieldVaultsStrategiesV2.cdc | 23 ------------------- 1 file changed, 23 deletions(-) diff --git a/cadence/contracts/FlowYieldVaultsStrategiesV2.cdc b/cadence/contracts/FlowYieldVaultsStrategiesV2.cdc index 7a6320d5..6b0bb943 100644 --- a/cadence/contracts/FlowYieldVaultsStrategiesV2.cdc +++ b/cadence/contracts/FlowYieldVaultsStrategiesV2.cdc @@ -48,8 +48,6 @@ access(all) contract FlowYieldVaultsStrategiesV2 { /// "syWFLOWvDebtTokenTypes" → {UInt64: Type} /// "moreERC4626Configs" → {Type: {Type: {Type: MoreERC4626CollateralConfig}}} - // @deprecated - should be used only for working with legacy positions - /// "originalCollateralTypes" → {UInt64: Type} access(contract) let config: {String: AnyStruct} /// Canonical StoragePath where the StrategyComposerIssuer should be stored @@ -2018,27 +2016,6 @@ 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 - } - init( univ3FactoryEVMAddress: String, univ3RouterEVMAddress: String, From 24f9d9907a9bd1da83b9b8d2b36005b469694737 Mon Sep 17 00:00:00 2001 From: Alex <12097569+nialexsan@users.noreply.github.com> Date: Tue, 31 Mar 2026 09:44:05 -0400 Subject: [PATCH 58/72] remove refs to preswap config --- ...wYieldVaultsStrategiesV2_syWFLOWv_test.cdc | 149 ++++++++++++++++-- local/setup_mainnet.sh | 9 -- local/setup_testnet.sh | 24 --- 3 files changed, 133 insertions(+), 49 deletions(-) diff --git a/cadence/tests/FlowYieldVaultsStrategiesV2_syWFLOWv_test.cdc b/cadence/tests/FlowYieldVaultsStrategiesV2_syWFLOWv_test.cdc index e6eda72e..9252be49 100644 --- a/cadence/tests/FlowYieldVaultsStrategiesV2_syWFLOWv_test.cdc +++ b/cadence/tests/FlowYieldVaultsStrategiesV2_syWFLOWv_test.cdc @@ -109,6 +109,23 @@ fun _latestVaultID(_ user: Test.TestAccount): UInt64 { return ids![ids!.length - 1] } +/// Returns the syWFLOWv share balance held in the AutoBalancer for the given vault ID, +/// or nil if no AutoBalancer exists. +access(all) +fun _autoBalancerBalance(_ vaultID: UInt64): UFix64? { + let r = _executeScript("../scripts/flow-yield-vaults/get_auto_balancer_balance_by_id.cdc", [vaultID]) + Test.expect(r, Test.beSucceeded()) + return r.returnValue! as! UFix64? +} + +/// Returns the WETH Cadence vault balance for the given account. +access(all) +fun _wethBalance(_ user: Test.TestAccount): UFix64 { + let r = _executeScript("../scripts/tokens/get_vault_balance_by_type.cdc", [user.address, wethVaultIdentifier]) + Test.expect(r, Test.beSucceeded()) + return (r.returnValue! as! UFix64?) ?? 0.0 +} + /* --- Setup --- */ access(all) fun setup() { @@ -250,22 +267,6 @@ 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( @@ -635,3 +636,119 @@ access(all) fun testCannotDepositWrongTokenToYieldVault() { Test.expect(depositResult, Test.beFailed()) log("Correctly rejected wrong-token deposit (WBTC into WETH vault)") } + +/* ========================================================= + Excess-yield test + ========================================================= */ + +/// Opens a syWFLOWvStrategy WETH vault, injects extra syWFLOWv to create an excess scenario, +/// closes the vault, and verifies the resulting collateral return and excess conversion behaviour. +/// +/// Scenario: +/// 1. Open a syWFLOWvStrategy vault with 0.001 WETH. +/// 2. Convert 50 FLOW → syWFLOWv and deposit directly into the AutoBalancer. +/// → AutoBalancer balance now exceeds what is needed to repay the FLOW debt. +/// 3. Close the vault. +/// → Step 8 of closePosition() drains the remaining syWFLOWv, converts it +/// syWFLOWv → FLOW → WETH, and adds it to the returned collateral. +/// 4. Verify collateral is returned and the user gains WETH from the excess. +access(all) fun testCloseSyWFLOWvVaultWithExcessYieldTokens_WETH() { + log("=== testCloseSyWFLOWvVaultWithExcessYieldTokens_WETH ===") + + let wethBefore = _wethBalance(wethUser) + log("WETH balance before vault creation: ".concat(wethBefore.toString())) + + let collateralAmount: UFix64 = 0.001 + log("Creating syWFLOWvStrategy vault with ".concat(collateralAmount.toString()).concat(" WETH...")) + let createResult = _executeTransactionFile( + "../transactions/flow-yield-vaults/create_yield_vault.cdc", + [syWFLOWvStrategyIdentifier, wethVaultIdentifier, collateralAmount], + [wethUser] + ) + Test.expect(createResult, Test.beSucceeded()) + + let vaultID = _latestVaultID(wethUser) + log("Created vault ID: ".concat(vaultID.toString())) + + let vaultBalAfterCreate = _executeScript( + "../scripts/flow-yield-vaults/get_yield_vault_balance.cdc", + [wethUser.address, vaultID] + ) + Test.expect(vaultBalAfterCreate, Test.beSucceeded()) + let vaultBal = vaultBalAfterCreate.returnValue! as! UFix64? + Test.assert(vaultBal != nil && vaultBal! > 0.0, + message: "Expected positive vault balance after create, got: ".concat((vaultBal ?? 0.0).toString())) + log("Vault balance (WETH collateral value): ".concat(vaultBal!.toString())) + + let abBalBefore = _autoBalancerBalance(vaultID) + Test.assert(abBalBefore != nil && abBalBefore! > 0.0, + message: "Expected positive AutoBalancer balance after vault creation, got: ".concat((abBalBefore ?? 0.0).toString())) + log("AutoBalancer syWFLOWv balance before injection: ".concat(abBalBefore!.toString())) + + // Convert 50 FLOW → syWFLOWv and inject into the AutoBalancer. + let injectionFlowAmount: UFix64 = 50.0 + log("Injecting ".concat(injectionFlowAmount.toString()).concat(" FLOW worth of syWFLOWv into AutoBalancer...")) + let injectResult = _executeTransactionFile( + "transactions/inject_syWFLOWv_to_autobalancer.cdc", + [vaultID, syWFLOWvEVMAddress, injectionFlowAmount], + [wethUser] + ) + Test.expect(injectResult, Test.beSucceeded()) + + let abBalAfter = _autoBalancerBalance(vaultID) + Test.assert(abBalAfter != nil, + message: "AutoBalancer should still exist after injection") + Test.assert(abBalAfter! > abBalBefore!, + message: "AutoBalancer balance should have increased after injection. Before: " + .concat(abBalBefore!.toString()).concat(" After: ").concat(abBalAfter!.toString())) + let injectedShares = abBalAfter! - abBalBefore! + log("AutoBalancer syWFLOWv balance after injection: ".concat(abBalAfter!.toString())) + log("Injected ".concat(injectedShares.toString()).concat(" syWFLOWv shares (excess over original debt coverage)")) + + log("Closing vault ".concat(vaultID.toString()).concat("...")) + let closeResult = _executeTransactionFile( + "../transactions/flow-yield-vaults/close_yield_vault.cdc", + [vaultID], + [wethUser] + ) + Test.expect(closeResult, Test.beSucceeded()) + + let vaultBalAfterClose = _executeScript( + "../scripts/flow-yield-vaults/get_yield_vault_balance.cdc", + [wethUser.address, vaultID] + ) + Test.expect(vaultBalAfterClose, Test.beSucceeded()) + Test.assert(vaultBalAfterClose.returnValue! as! UFix64? == nil, + message: "Vault ".concat(vaultID.toString()).concat(" should not exist after close")) + log("Vault no longer exists — close confirmed") + + let abBalFinal = _autoBalancerBalance(vaultID) + Test.assert(abBalFinal == nil, + message: "AutoBalancer should be nil (burned) after vault close, but got: ".concat((abBalFinal ?? 0.0).toString())) + log("AutoBalancer is nil after close — torn down during _cleanupAutoBalancer") + + let wethAfter = _wethBalance(wethUser) + log("WETH balance after close: ".concat(wethAfter.toString())) + + let tolerance: UFix64 = collateralAmount * 0.05 + Test.assert( + wethAfter >= wethBefore - tolerance, + message: "User should have received ~".concat(collateralAmount.toString()) + .concat(" WETH back (minus swap fees). Before: ").concat(wethBefore.toString()) + .concat(", After: ").concat(wethAfter.toString()) + .concat(", Expected min: ").concat((wethBefore - tolerance).toString()) + ) + + // 50 FLOW ≈ $35–50 ≈ 0.012–0.017 WETH — well above any fee loss on 0.001 WETH collateral. + Test.assert( + wethAfter > wethBefore, + message: "User should have received MORE WETH than before (excess syWFLOWv converted to collateral). " + .concat("Before: ").concat(wethBefore.toString()) + .concat(", After: ").concat(wethAfter.toString()) + ) + let wethNet = wethAfter - wethBefore + log("Net WETH gain from excess syWFLOWv conversion: ".concat(wethNet.toString()) + .concat(" WETH (injected ≈").concat(injectionFlowAmount.toString()).concat(" FLOW worth)")) + + log("=== testCloseSyWFLOWvVaultWithExcessYieldTokens_WETH PASSED ===") +} diff --git a/local/setup_mainnet.sh b/local/setup_mainnet.sh index af618594..ff684e18 100755 --- a/local/setup_mainnet.sh +++ b/local/setup_mainnet.sh @@ -201,15 +201,6 @@ flow transactions send ./cadence/transactions/flow-yield-vaults/admin/upsert_mor --network mainnet \ --signer mainnet-admin -# MOET pre-swap: PYUSD0→MOET via UniV3 fee 100 (FlowALP only accepts MOET as stablecoin collateral) -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' \ diff --git a/local/setup_testnet.sh b/local/setup_testnet.sh index ef422029..dd1f973a 100755 --- a/local/setup_testnet.sh +++ b/local/setup_testnet.sh @@ -163,18 +163,6 @@ flow transactions send ./cadence/transactions/flow-yield-vaults/admin/upsert_str # --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' \ @@ -235,18 +223,6 @@ flow transactions send ./cadence/transactions/flow-yield-vaults/admin/upsert_mor --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' \ From 74361e932bd3d99e1594717a3f78f8c6d9445d87 Mon Sep 17 00:00:00 2001 From: Alex <12097569+nialexsan@users.noreply.github.com> Date: Tue, 31 Mar 2026 09:51:26 -0400 Subject: [PATCH 59/72] remove unused seed pool script --- .../transactions/seed_pool_moet_to_pyusd0.cdc | 89 ------------------- 1 file changed, 89 deletions(-) delete mode 100644 cadence/tests/transactions/seed_pool_moet_to_pyusd0.cdc diff --git a/cadence/tests/transactions/seed_pool_moet_to_pyusd0.cdc b/cadence/tests/transactions/seed_pool_moet_to_pyusd0.cdc deleted file mode 100644 index 9fc8e8e9..00000000 --- a/cadence/tests/transactions/seed_pool_moet_to_pyusd0.cdc +++ /dev/null @@ -1,89 +0,0 @@ -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) - } -} From 8953145059fd7a1f50e3bea91e2e71a672152ffb Mon Sep 17 00:00:00 2001 From: Alex <12097569+nialexsan@users.noreply.github.com> Date: Tue, 31 Mar 2026 10:34:36 -0400 Subject: [PATCH 60/72] sywflow tests --- .../tokens/get_vault_balance_by_type.cdc | 33 +++++++++ ...wYieldVaultsStrategiesV2_syWFLOWv_test.cdc | 6 +- .../inject_syWFLOWv_to_autobalancer.cdc | 71 +++++++++++++++++++ 3 files changed, 107 insertions(+), 3 deletions(-) create mode 100644 cadence/scripts/tokens/get_vault_balance_by_type.cdc create mode 100644 cadence/tests/transactions/inject_syWFLOWv_to_autobalancer.cdc diff --git a/cadence/scripts/tokens/get_vault_balance_by_type.cdc b/cadence/scripts/tokens/get_vault_balance_by_type.cdc new file mode 100644 index 00000000..8acd64f3 --- /dev/null +++ b/cadence/scripts/tokens/get_vault_balance_by_type.cdc @@ -0,0 +1,33 @@ +import "FungibleToken" +import "FungibleTokenMetadataViews" +import "ViewResolver" + +/// Returns the Cadence FungibleToken balance for the given account, resolving the +/// vault's public metadata path dynamically via the FTVaultData metadata view. +/// +/// Useful for checking balances of VM-bridged ERC-20 tokens without hard-coding public paths. +/// +/// @param address: The account address to check +/// @param vaultIdentifier: The Cadence type identifier (e.g. "A.1e4aa0b87d10b141.EVMVMBridgedToken_...Vault") +/// @return UFix64?: The vault balance, or nil if the vault is not set up for this account +/// +access(all) fun main(address: Address, vaultIdentifier: String): UFix64? { + let vaultType = CompositeType(vaultIdentifier) + if vaultType == nil { return nil } + + let contractAddr = vaultType!.address + if contractAddr == nil { return nil } + let contractName = vaultType!.contractName + if contractName == nil { return nil } + + let viewResolver = getAccount(contractAddr!).contracts.borrow<&{ViewResolver}>(name: contractName!) + if viewResolver == nil { return nil } + + let vaultData = viewResolver!.resolveContractView( + resourceType: vaultType!, + viewType: Type() + ) as? FungibleTokenMetadataViews.FTVaultData + if vaultData == nil { return nil } + + return getAccount(address).capabilities.borrow<&{FungibleToken.Vault}>(vaultData!.metadataPath)?.balance +} diff --git a/cadence/tests/FlowYieldVaultsStrategiesV2_syWFLOWv_test.cdc b/cadence/tests/FlowYieldVaultsStrategiesV2_syWFLOWv_test.cdc index 9252be49..93f6a80c 100644 --- a/cadence/tests/FlowYieldVaultsStrategiesV2_syWFLOWv_test.cdc +++ b/cadence/tests/FlowYieldVaultsStrategiesV2_syWFLOWv_test.cdc @@ -115,7 +115,7 @@ access(all) fun _autoBalancerBalance(_ vaultID: UInt64): UFix64? { let r = _executeScript("../scripts/flow-yield-vaults/get_auto_balancer_balance_by_id.cdc", [vaultID]) Test.expect(r, Test.beSucceeded()) - return r.returnValue! as! UFix64? + return r.returnValue as? UFix64 } /// Returns the WETH Cadence vault balance for the given account. @@ -123,7 +123,7 @@ access(all) fun _wethBalance(_ user: Test.TestAccount): UFix64 { let r = _executeScript("../scripts/tokens/get_vault_balance_by_type.cdc", [user.address, wethVaultIdentifier]) Test.expect(r, Test.beSucceeded()) - return (r.returnValue! as! UFix64?) ?? 0.0 + return (r.returnValue as? UFix64) ?? 0.0 } /* --- Setup --- */ @@ -718,7 +718,7 @@ access(all) fun testCloseSyWFLOWvVaultWithExcessYieldTokens_WETH() { [wethUser.address, vaultID] ) Test.expect(vaultBalAfterClose, Test.beSucceeded()) - Test.assert(vaultBalAfterClose.returnValue! as! UFix64? == nil, + Test.assert(vaultBalAfterClose.returnValue == nil, message: "Vault ".concat(vaultID.toString()).concat(" should not exist after close")) log("Vault no longer exists — close confirmed") diff --git a/cadence/tests/transactions/inject_syWFLOWv_to_autobalancer.cdc b/cadence/tests/transactions/inject_syWFLOWv_to_autobalancer.cdc new file mode 100644 index 00000000..8f8cb7f4 --- /dev/null +++ b/cadence/tests/transactions/inject_syWFLOWv_to_autobalancer.cdc @@ -0,0 +1,71 @@ +import "FungibleToken" +import "FlowToken" +import "EVM" +import "FungibleTokenConnectors" +import "ERC4626SwapConnectors" +import "FlowYieldVaultsAutoBalancersV1" + +/// Converts FLOW → syWFLOWv (via ERC4626 deposit on the More vault) and injects the +/// resulting shares directly into the AutoBalancer for the given yield vault ID. +/// +/// This simulates accumulated yield in the AutoBalancer, producing a scenario where +/// the yield token (syWFLOWv) balance is greater than the vault's outstanding FLOW debt. +/// +/// The signer must hold FLOW in their /storage/flowTokenVault and have a COA at /storage/evm. +/// +/// @param vaultID: The YieldVault ID whose AutoBalancer should receive extra syWFLOWv +/// @param syWFLOWvEVMAddr: EVM address of the syWFLOWv ERC4626 vault (hex with 0x prefix) +/// @param flowAmount: Amount of FLOW to convert to syWFLOWv and inject +/// +transaction( + vaultID: UInt64, + syWFLOWvEVMAddr: String, + flowAmount: UFix64 +) { + prepare(signer: auth(Storage, BorrowValue, IssueStorageCapabilityController) &Account) { + // Issue a COA capability with EVM.Call and EVM.Bridge entitlements for the ERC4626 deposit + let coaCap = signer.capabilities.storage.issue(/storage/evm) + + // Issue a withdraw capability on the signer's FLOW vault to serve as the fee source + let flowWithdrawCap = signer.capabilities.storage.issue(/storage/flowTokenVault) + let feeSource = FungibleTokenConnectors.VaultSinkAndSource( + min: nil, + max: nil, + vault: flowWithdrawCap, + uniqueID: nil + ) + + // Build ERC4626 swapper: FLOW (Cadence) → syWFLOWv (ERC4626 deposit via WFLOW bridge) + // asset = FlowToken.Vault whose EVM counterpart is WFLOW — the underlying of syWFLOWv + let swapper = ERC4626SwapConnectors.Swapper( + asset: Type<@FlowToken.Vault>(), + vault: EVM.addressFromString(syWFLOWvEVMAddr), + coa: coaCap, + feeSource: feeSource, + uniqueID: nil + ) + + // Get a quote for converting flowAmount FLOW → syWFLOWv shares + let quote = swapper.quoteOut(forProvided: flowAmount, reverse: false) + assert( + quote.outAmount > 0.0, + message: "FLOW → syWFLOWv quote returned zero — syWFLOWv vault may be at capacity" + ) + + // Withdraw FLOW from signer's vault + let flowVault = signer.storage.borrow(from: /storage/flowTokenVault) + ?? panic("No FLOW vault found at /storage/flowTokenVault") + let flowIn <- flowVault.withdraw(amount: flowAmount) + + // Swap FLOW → syWFLOWv (bridges to EVM, deposits into syWFLOWv, bridges back to Cadence) + let syWFLOWvOut <- swapper.swap(quote: quote, inVault: <-flowIn) + log("Converted ".concat(flowAmount.toString()).concat(" FLOW → ").concat(syWFLOWvOut.balance.toString()).concat(" syWFLOWv shares")) + + // Deposit the syWFLOWv shares directly into the vault's AutoBalancer. + // AutoBalancers.AutoBalancer.deposit() is access(all) — callable via the public reference. + let autoBalancer = FlowYieldVaultsAutoBalancersV1.borrowAutoBalancer(id: vaultID) + ?? panic("No AutoBalancer found for vault ID ".concat(vaultID.toString())) + autoBalancer.deposit(from: <-syWFLOWvOut) + log("Injected syWFLOWv into AutoBalancer for vault ID ".concat(vaultID.toString())) + } +} From 35dbf52c6876039b8a0b24d6c27d5b87927f6f02 Mon Sep 17 00:00:00 2001 From: Alex <12097569+nialexsan@users.noreply.github.com> Date: Tue, 31 Mar 2026 10:53:35 -0400 Subject: [PATCH 61/72] add excess yield token tests --- ...lowYieldVaultsStrategiesV2_FUSDEV_test.cdc | 143 ++++++++++++++++ ...wYieldVaultsStrategiesV2_syWFLOWv_test.cdc | 2 +- ...ject_flow_as_sywflowv_to_autobalancer.cdc} | 0 ...nject_pyusd0_as_fusdev_to_autobalancer.cdc | 88 ++++++++++ .../transactions/swap_flow_to_pyusd0.cdc | 159 ------------------ .../tests/transactions/transfer_pyusd0.cdc | 54 ++++++ 6 files changed, 286 insertions(+), 160 deletions(-) rename cadence/tests/transactions/{inject_syWFLOWv_to_autobalancer.cdc => inject_flow_as_sywflowv_to_autobalancer.cdc} (100%) create mode 100644 cadence/tests/transactions/inject_pyusd0_as_fusdev_to_autobalancer.cdc delete mode 100644 cadence/tests/transactions/swap_flow_to_pyusd0.cdc create mode 100644 cadence/tests/transactions/transfer_pyusd0.cdc diff --git a/cadence/tests/FlowYieldVaultsStrategiesV2_FUSDEV_test.cdc b/cadence/tests/FlowYieldVaultsStrategiesV2_FUSDEV_test.cdc index 83cdf6d6..d506ca27 100644 --- a/cadence/tests/FlowYieldVaultsStrategiesV2_FUSDEV_test.cdc +++ b/cadence/tests/FlowYieldVaultsStrategiesV2_FUSDEV_test.cdc @@ -119,6 +119,23 @@ fun _latestVaultID(_ user: Test.TestAccount): UInt64 { return ids![ids!.length - 1] } +/// Returns the FUSDEV share balance held in the AutoBalancer for the given vault ID, +/// or nil if no AutoBalancer exists. +access(all) +fun _autoBalancerBalance(_ vaultID: UInt64): UFix64? { + let r = _executeScript("../scripts/flow-yield-vaults/get_auto_balancer_balance_by_id.cdc", [vaultID]) + Test.expect(r, Test.beSucceeded()) + return r.returnValue as? UFix64 +} + +/// Returns the WETH Cadence vault balance for the given account. +access(all) +fun _wethBalance(_ user: Test.TestAccount): UFix64 { + let r = _executeScript("../scripts/tokens/get_vault_balance_by_type.cdc", [user.address, wethVaultIdentifier]) + Test.expect(r, Test.beSucceeded()) + return (r.returnValue as? UFix64) ?? 0.0 +} + /* --- Setup --- */ access(all) fun setup() { @@ -639,3 +656,129 @@ access(all) fun testCannotDepositWrongTokenToYieldVault() { Test.expect(depositResult, Test.beFailed()) log("Correctly rejected wrong-token deposit (WBTC into WETH vault)") } + +/* ========================================================= + Excess-yield tests + ========================================================= */ + +/// Opens a FUSDEVStrategy WETH vault, injects extra FUSDEV to create an excess scenario, +/// closes the vault, and verifies the resulting collateral return and excess burn behaviour. +/// +/// Scenario: +/// 1. Open a FUSDEVStrategy vault with 0.001 WETH. +/// 2. Swap 5 FLOW → PYUSD0 via EVM UniV3 WFLOW/PYUSD0 pool. +/// 3. Convert PYUSD0 → FUSDEV and deposit directly into the AutoBalancer (public deposit()). +/// → AutoBalancer balance now exceeds what is needed to repay the MOET debt. +/// 4. Close the vault. +/// → Step 9 of closePosition() drains the remaining FUSDEV, converts it +/// FUSDEV → MOET → collateral, and adds it to the returned collateral. +/// 5. Verify collateral is returned and the user gains WETH from the excess. +access(all) fun testCloseFUSDEVVaultWithExcessYieldTokens_WETH() { + log("=== testCloseFUSDEVVaultWithExcessYieldTokens_WETH ===") + + let wethBefore = _wethBalance(wethUser) + log("WETH balance before vault creation: ".concat(wethBefore.toString())) + + let collateralAmount: UFix64 = 0.001 + log("Creating FUSDEVStrategy vault with ".concat(collateralAmount.toString()).concat(" WETH...")) + let createResult = _executeTransactionFile( + "../transactions/flow-yield-vaults/create_yield_vault.cdc", + [fusdEvStrategyIdentifier, wethVaultIdentifier, collateralAmount], + [wethUser] + ) + Test.expect(createResult, Test.beSucceeded()) + + let vaultID = _latestVaultID(wethUser) + log("Created vault ID: ".concat(vaultID.toString())) + + let vaultBalAfterCreate = _executeScript( + "../scripts/flow-yield-vaults/get_yield_vault_balance.cdc", + [wethUser.address, vaultID] + ) + Test.expect(vaultBalAfterCreate, Test.beSucceeded()) + let vaultBal = vaultBalAfterCreate.returnValue! as! UFix64? + Test.assert(vaultBal != nil && vaultBal! > 0.0, + message: "Expected positive vault balance after create, got: ".concat((vaultBal ?? 0.0).toString())) + log("Vault balance (WETH collateral value): ".concat(vaultBal!.toString())) + + let abBalBefore = _autoBalancerBalance(vaultID) + Test.assert(abBalBefore != nil && abBalBefore! > 0.0, + message: "Expected positive AutoBalancer balance after vault creation, got: ".concat((abBalBefore ?? 0.0).toString())) + log("AutoBalancer FUSDEV balance before injection: ".concat(abBalBefore!.toString())) + + // Transfer PYUSD0 from pyusd0Holder to wethUser (avoids EVM swap; pyusd0Holder has ~70k PYUSD0). + let injectionPYUSD0Amount: UFix64 = 5.0 + log("Transferring ".concat(injectionPYUSD0Amount.toString()).concat(" PYUSD0 from pyusd0Holder to wethUser...")) + let transferResult = _executeTransactionFile( + "transactions/transfer_pyusd0.cdc", + [injectionPYUSD0Amount], + [pyusd0Holder, wethUser] + ) + Test.expect(transferResult, Test.beSucceeded()) + + // Inject PYUSD0 → FUSDEV into the AutoBalancer. + log("Injecting ".concat(injectionPYUSD0Amount.toString()).concat(" PYUSD0 worth of FUSDEV into AutoBalancer...")) + let injectResult = _executeTransactionFile( + "transactions/inject_pyusd0_as_fusdev_to_autobalancer.cdc", + [vaultID, fusdEvEVMAddress, injectionPYUSD0Amount], + [wethUser] + ) + Test.expect(injectResult, Test.beSucceeded()) + + let abBalAfter = _autoBalancerBalance(vaultID) + Test.assert(abBalAfter != nil, + message: "AutoBalancer should still exist after injection") + Test.assert(abBalAfter! > abBalBefore!, + message: "AutoBalancer FUSDEV balance should have increased after injection. Before: " + .concat(abBalBefore!.toString()).concat(" After: ").concat(abBalAfter!.toString())) + let injectedShares = abBalAfter! - abBalBefore! + log("AutoBalancer FUSDEV balance after injection: ".concat(abBalAfter!.toString())) + log("Injected ".concat(injectedShares.toString()).concat(" FUSDEV shares (excess over original debt coverage)")) + + log("Closing vault ".concat(vaultID.toString()).concat("...")) + let closeResult = _executeTransactionFile( + "../transactions/flow-yield-vaults/close_yield_vault.cdc", + [vaultID], + [wethUser] + ) + Test.expect(closeResult, Test.beSucceeded()) + + let vaultBalAfterClose = _executeScript( + "../scripts/flow-yield-vaults/get_yield_vault_balance.cdc", + [wethUser.address, vaultID] + ) + Test.expect(vaultBalAfterClose, Test.beSucceeded()) + Test.assert(vaultBalAfterClose.returnValue == nil, + message: "Vault ".concat(vaultID.toString()).concat(" should not exist after close")) + log("Vault no longer exists — close confirmed") + + let abBalFinal = _autoBalancerBalance(vaultID) + Test.assert(abBalFinal == nil, + message: "AutoBalancer should be nil (burned) after vault close, but got: ".concat((abBalFinal ?? 0.0).toString())) + log("AutoBalancer is nil after close — torn down during _cleanupAutoBalancer") + + let wethAfter = _wethBalance(wethUser) + log("WETH balance after close: ".concat(wethAfter.toString())) + + let tolerance: UFix64 = collateralAmount * 0.05 + Test.assert( + wethAfter >= wethBefore - tolerance, + message: "User should have received ~".concat(collateralAmount.toString()) + .concat(" WETH back (minus swap fees). Before: ").concat(wethBefore.toString()) + .concat(", After: ").concat(wethAfter.toString()) + .concat(", Expected min: ").concat((wethBefore - tolerance).toString()) + ) + + // 5 PYUSD0 ≈ $5 at current prices ≈ 0.002 WETH — well above any fee loss on 0.001 WETH collateral. + Test.assert( + wethAfter > wethBefore, + message: "User should have received MORE WETH than before (excess FUSDEV converted to collateral). " + .concat("Before: ").concat(wethBefore.toString()) + .concat(", After: ").concat(wethAfter.toString()) + ) + let wethNet = wethAfter - wethBefore + log("Net WETH gain from excess FUSDEV conversion: ".concat(wethNet.toString()) + .concat(" WETH (injected ≈").concat(injectionPYUSD0Amount.toString()).concat(" PYUSD0 worth)")) + + log("=== testCloseFUSDEVVaultWithExcessYieldTokens_WETH PASSED ===") +} diff --git a/cadence/tests/FlowYieldVaultsStrategiesV2_syWFLOWv_test.cdc b/cadence/tests/FlowYieldVaultsStrategiesV2_syWFLOWv_test.cdc index 93f6a80c..dc737ff2 100644 --- a/cadence/tests/FlowYieldVaultsStrategiesV2_syWFLOWv_test.cdc +++ b/cadence/tests/FlowYieldVaultsStrategiesV2_syWFLOWv_test.cdc @@ -689,7 +689,7 @@ access(all) fun testCloseSyWFLOWvVaultWithExcessYieldTokens_WETH() { let injectionFlowAmount: UFix64 = 50.0 log("Injecting ".concat(injectionFlowAmount.toString()).concat(" FLOW worth of syWFLOWv into AutoBalancer...")) let injectResult = _executeTransactionFile( - "transactions/inject_syWFLOWv_to_autobalancer.cdc", + "transactions/inject_flow_as_sywflowv_to_autobalancer.cdc", [vaultID, syWFLOWvEVMAddress, injectionFlowAmount], [wethUser] ) diff --git a/cadence/tests/transactions/inject_syWFLOWv_to_autobalancer.cdc b/cadence/tests/transactions/inject_flow_as_sywflowv_to_autobalancer.cdc similarity index 100% rename from cadence/tests/transactions/inject_syWFLOWv_to_autobalancer.cdc rename to cadence/tests/transactions/inject_flow_as_sywflowv_to_autobalancer.cdc diff --git a/cadence/tests/transactions/inject_pyusd0_as_fusdev_to_autobalancer.cdc b/cadence/tests/transactions/inject_pyusd0_as_fusdev_to_autobalancer.cdc new file mode 100644 index 00000000..3e107f6e --- /dev/null +++ b/cadence/tests/transactions/inject_pyusd0_as_fusdev_to_autobalancer.cdc @@ -0,0 +1,88 @@ +import "FungibleToken" +import "FungibleTokenMetadataViews" +import "ViewResolver" +import "FlowToken" +import "EVM" +import "FlowEVMBridgeConfig" +import "FungibleTokenConnectors" +import "MorphoERC4626SwapConnectors" +import "FlowYieldVaultsAutoBalancersV1" + +/// Converts PYUSD0 → FUSDEV (via Morpho ERC4626 deposit) and injects the resulting +/// shares directly into the AutoBalancer for the given yield vault ID. +/// +/// This simulates accumulated yield in the AutoBalancer, producing a scenario where +/// the yield token (FUSDEV) balance is greater than the vault's outstanding MOET debt. +/// +/// The signer must hold PYUSD0 in their Cadence vault and have a COA at /storage/evm. +/// FLOW at /storage/flowTokenVault is used for bridge fees. +/// +/// @param vaultID: The YieldVault ID whose AutoBalancer should receive FUSDEV +/// @param fusdEvEVMAddr: EVM address of the FUSDEV Morpho ERC4626 vault (hex with 0x prefix) +/// @param pyusd0Amount: Amount of PYUSD0 to deposit into FUSDEV and inject as excess +/// +transaction( + vaultID: UInt64, + fusdEvEVMAddr: String, + pyusd0Amount: UFix64 +) { + prepare(signer: auth(Storage, BorrowValue, IssueStorageCapabilityController) &Account) { + // Issue COA capability (needs EVM.Call and EVM.Bridge for Morpho ERC4626 deposit) + let coaCap = signer.capabilities.storage.issue(/storage/evm) + + // Issue a withdraw capability on the signer's FLOW vault to serve as the bridge fee source + let flowWithdrawCap = signer.capabilities.storage.issue(/storage/flowTokenVault) + let feeSource = FungibleTokenConnectors.VaultSinkAndSource( + min: nil, + max: nil, + vault: flowWithdrawCap, + uniqueID: nil + ) + + // Build Morpho ERC4626 swapper: PYUSD0 → FUSDEV (isReversed: false) + // The Swapper auto-detects PYUSD0 as the underlying asset from the FUSDEV vault. + let swapper = MorphoERC4626SwapConnectors.Swapper( + vaultEVMAddress: EVM.addressFromString(fusdEvEVMAddr), + coa: coaCap, + feeSource: feeSource, + uniqueID: nil, + isReversed: false + ) + + // Locate the signer's PYUSD0 Cadence vault dynamically via FTVaultData. + let pyusd0EVM = EVM.addressFromString("0x99aF3EeA856556646C98c8B9b2548Fe815240750") + let pyusd0Type = FlowEVMBridgeConfig.getTypeAssociated(with: pyusd0EVM) + ?? panic("PYUSD0 EVM address not registered in bridge config") + 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") + + let pyusd0Provider = signer.storage.borrow( + from: pyusd0VaultData.storagePath + ) ?? panic("No PYUSD0 vault found at ".concat(pyusd0VaultData.storagePath.toString())) + + let pyusd0In <- pyusd0Provider.withdraw(amount: pyusd0Amount) + + // Quote and swap PYUSD0 → FUSDEV (Morpho ERC4626 deposit via EVM) + let quote = swapper.quoteOut(forProvided: pyusd0Amount, reverse: false) + assert( + quote.outAmount > 0.0, + message: "PYUSD0 → FUSDEV quote returned zero — Morpho vault may be at capacity" + ) + let fusdEvOut <- swapper.swap(quote: quote, inVault: <-pyusd0In) + log("Converted ".concat(pyusd0Amount.toString()).concat(" PYUSD0 → ").concat(fusdEvOut.balance.toString()).concat(" FUSDEV shares")) + + // Deposit FUSDEV shares directly into the vault's AutoBalancer. + // AutoBalancers.AutoBalancer.deposit() is access(all) — callable via the public reference. + let autoBalancer = FlowYieldVaultsAutoBalancersV1.borrowAutoBalancer(id: vaultID) + ?? panic("No AutoBalancer found for vault ID ".concat(vaultID.toString())) + autoBalancer.deposit(from: <-fusdEvOut) + log("Injected FUSDEV into AutoBalancer for vault ID ".concat(vaultID.toString())) + } +} diff --git a/cadence/tests/transactions/swap_flow_to_pyusd0.cdc b/cadence/tests/transactions/swap_flow_to_pyusd0.cdc deleted file mode 100644 index 8637a277..00000000 --- a/cadence/tests/transactions/swap_flow_to_pyusd0.cdc +++ /dev/null @@ -1,159 +0,0 @@ -import "FungibleToken" -import "FungibleTokenMetadataViews" -import "ViewResolver" -import "FlowToken" -import "EVM" -import "FlowEVMBridgeUtils" -import "FlowEVMBridgeConfig" -import "ScopedFTProviders" - -/// Funds the signer with PYUSD0 by swapping FLOW on EVM via UniswapV3, then bridging to Cadence. -/// -/// Steps: -/// 1. Deposit FLOW to the signer's COA -/// 2. Wrap FLOW to WFLOW -/// 3. Swap WFLOW -> PYUSD0 via UniswapV3 exactInput -/// 4. Bridge PYUSD0 from EVM to Cadence -/// -/// @param flowAmount: Amount of FLOW to swap for PYUSD0 -/// -transaction(flowAmount: UFix64) { - - let coa: auth(EVM.Owner, EVM.Call, EVM.Bridge) &EVM.CadenceOwnedAccount - let scopedProvider: @ScopedFTProviders.ScopedFTProvider - let receiver: &{FungibleToken.Vault} - - prepare(signer: auth(BorrowValue, SaveValue, IssueStorageCapabilityController, PublishCapability, UnpublishCapability, CopyValue) &Account) { - // Borrow COA - self.coa = signer.storage.borrow(from: /storage/evm) - ?? panic("No COA at /storage/evm") - - // Withdraw FLOW and deposit to COA - let flowVault = signer.storage.borrow(from: /storage/flowTokenVault) - ?? panic("No FlowToken vault") - let deposit <- flowVault.withdraw(amount: flowAmount) as! @FlowToken.Vault - self.coa.deposit(from: <-deposit) - - // Set up scoped fee provider for bridging - let approxFee = FlowEVMBridgeUtils.calculateBridgeFee(bytes: 400_000) - if signer.storage.type(at: FlowEVMBridgeConfig.providerCapabilityStoragePath) == nil { - let providerCap = signer.capabilities.storage.issue( - /storage/flowTokenVault - ) - signer.storage.save(providerCap, to: FlowEVMBridgeConfig.providerCapabilityStoragePath) - } - let providerCapCopy = signer.storage.copy>( - from: FlowEVMBridgeConfig.providerCapabilityStoragePath - ) ?? panic("Invalid provider capability") - self.scopedProvider <- ScopedFTProviders.createScopedFTProvider( - provider: providerCapCopy, - filters: [ScopedFTProviders.AllowanceFilter(approxFee)], - expiration: getCurrentBlock().timestamp + 1000.0 - ) - - // Set up PYUSD0 vault if needed - let pyusd0Type = CompositeType("A.1e4aa0b87d10b141.EVMVMBridgedToken_99af3eea856556646c98c8b9b2548fe815240750.Vault")! - let tokenContractAddress = FlowEVMBridgeUtils.getContractAddress(fromType: pyusd0Type) - ?? panic("Could not get PYUSD0 contract address") - let tokenContractName = FlowEVMBridgeUtils.getContractName(fromType: pyusd0Type) - ?? panic("Could not get PYUSD0 contract name") - let viewResolver = getAccount(tokenContractAddress).contracts.borrow<&{ViewResolver}>(name: tokenContractName) - ?? panic("Could not borrow ViewResolver for PYUSD0") - let vaultData = viewResolver.resolveContractView( - resourceType: pyusd0Type, - viewType: Type() - ) as! FungibleTokenMetadataViews.FTVaultData? - ?? panic("Could not resolve FTVaultData for PYUSD0") - if signer.storage.borrow<&{FungibleToken.Vault}>(from: vaultData.storagePath) == nil { - signer.storage.save(<-vaultData.createEmptyVault(), to: vaultData.storagePath) - signer.capabilities.unpublish(vaultData.receiverPath) - signer.capabilities.unpublish(vaultData.metadataPath) - let receiverCap = signer.capabilities.storage.issue<&{FungibleToken.Vault}>(vaultData.storagePath) - let metadataCap = signer.capabilities.storage.issue<&{FungibleToken.Vault}>(vaultData.storagePath) - signer.capabilities.publish(receiverCap, at: vaultData.receiverPath) - signer.capabilities.publish(metadataCap, at: vaultData.metadataPath) - } - self.receiver = signer.storage.borrow<&{FungibleToken.Vault}>(from: vaultData.storagePath) - ?? panic("Could not borrow PYUSD0 vault") - } - - execute { - let wflowAddr = EVM.addressFromString("0xd3bF53DAC106A0290B0483EcBC89d40FcC961f3e") - let pyusd0Addr = EVM.addressFromString("0x99aF3EeA856556646C98c8B9b2548Fe815240750") - let routerAddr = EVM.addressFromString("0xeEDC6Ff75e1b10B903D9013c358e446a73d35341") - let zeroValue = EVM.Balance(attoflow: 0) - - // 1. Wrap FLOW -> WFLOW - let flowBalance = EVM.Balance(attoflow: 0) - flowBalance.setFLOW(flow: flowAmount) - let wrapRes = self.coa.call( - to: wflowAddr, - data: EVM.encodeABIWithSignature("deposit()", []), - gasLimit: 100_000, - value: flowBalance - ) - assert(wrapRes.status == EVM.Status.successful, message: "WFLOW wrap failed: ".concat(wrapRes.errorMessage)) - - // 2. Approve UniV3 Router to spend WFLOW - let amountEVM = FlowEVMBridgeUtils.convertCadenceAmountToERC20Amount(flowAmount, erc20Address: wflowAddr) - let approveRes = self.coa.call( - to: wflowAddr, - data: EVM.encodeABIWithSignature("approve(address,uint256)", [routerAddr, amountEVM]), - gasLimit: 100_000, - value: zeroValue - ) - assert(approveRes.status == EVM.Status.successful, message: "WFLOW approve failed: ".concat(approveRes.errorMessage)) - - // 3. Swap WFLOW -> PYUSD0 via UniV3 exactInput - // Path encoding: tokenIn(20) | fee(3 bytes big-endian) | tokenOut(20) - var pathBytes: [UInt8] = [] - let wflowFixed: [UInt8; 20] = wflowAddr.bytes - let pyusd0Fixed: [UInt8; 20] = pyusd0Addr.bytes - var i = 0 - while i < 20 { pathBytes.append(wflowFixed[i]); i = i + 1 } - // fee 3000 = 0x000BB8 big-endian - pathBytes.append(0x00) - pathBytes.append(0x0B) - pathBytes.append(0xB8) - i = 0 - while i < 20 { pathBytes.append(pyusd0Fixed[i]); i = i + 1 } - - let swapRes = self.coa.call( - to: routerAddr, - data: EVM.encodeABIWithSignature( - "exactInput((bytes,address,uint256,uint256))", - [EVM.EVMBytes(value: pathBytes), self.coa.address(), amountEVM, UInt256(0)] - ), - gasLimit: 1_000_000, - value: zeroValue - ) - assert(swapRes.status == EVM.Status.successful, message: "UniV3 swap failed: ".concat(swapRes.errorMessage)) - - // 4. Check PYUSD0 balance in COA - let balRes = self.coa.call( - to: pyusd0Addr, - data: EVM.encodeABIWithSignature("balanceOf(address)", [self.coa.address()]), - gasLimit: 100_000, - value: zeroValue - ) - assert(balRes.status == EVM.Status.successful, message: "balanceOf failed") - let decoded = EVM.decodeABI(types: [Type()], data: balRes.data) - let pyusd0Balance = decoded[0] as! UInt256 - assert(pyusd0Balance > UInt256(0), message: "No PYUSD0 received from swap") - - // 5. Bridge PYUSD0 from EVM to Cadence - let pyusd0Type = CompositeType("A.1e4aa0b87d10b141.EVMVMBridgedToken_99af3eea856556646c98c8b9b2548fe815240750.Vault")! - let bridgedVault <- self.coa.withdrawTokens( - type: pyusd0Type, - amount: pyusd0Balance, - feeProvider: &self.scopedProvider as auth(FungibleToken.Withdraw) &{FungibleToken.Provider} - ) - assert(bridgedVault.balance > 0.0, message: "Bridged PYUSD0 vault is empty") - log("Bridged PYUSD0 amount: ".concat(bridgedVault.balance.toString())) - - // Deposit bridged PYUSD0 into the signer's Cadence vault - self.receiver.deposit(from: <-bridgedVault) - - destroy self.scopedProvider - } -} diff --git a/cadence/tests/transactions/transfer_pyusd0.cdc b/cadence/tests/transactions/transfer_pyusd0.cdc new file mode 100644 index 00000000..ed5ea036 --- /dev/null +++ b/cadence/tests/transactions/transfer_pyusd0.cdc @@ -0,0 +1,54 @@ +import "FungibleToken" +import "FungibleTokenMetadataViews" +import "ViewResolver" + +/// Transfers PYUSD0 from the first signer (sender) to the second signer (receiver). +/// Sets up the receiver's PYUSD0 Cadence vault if it is not already present. +/// +/// Used in tests to provision PYUSD0 to accounts that have a COA but no PYUSD0, +/// avoiding the need for an EVM swap. +/// +/// @param amount: PYUSD0 amount (UFix64) to transfer +transaction(amount: UFix64) { + + let vault: @{FungibleToken.Vault} + let receiver: &{FungibleToken.Vault} + + prepare( + sender: auth(BorrowValue) &Account, + rcvr: auth(Storage, IssueStorageCapabilityController, PublishCapability, UnpublishCapability) &Account + ) { + let pyusd0Type = CompositeType("A.1e4aa0b87d10b141.EVMVMBridgedToken_99af3eea856556646c98c8b9b2548fe815240750.Vault")! + let viewResolver = getAccount(pyusd0Type.address!).contracts.borrow<&{ViewResolver}>(name: pyusd0Type.contractName!) + ?? panic("Could not borrow ViewResolver for PYUSD0") + let vaultData = viewResolver.resolveContractView( + resourceType: pyusd0Type, + viewType: Type() + ) as! FungibleTokenMetadataViews.FTVaultData? + ?? panic("Could not resolve FTVaultData for PYUSD0") + + // Withdraw from sender + let senderVault = sender.storage.borrow( + from: vaultData.storagePath + ) ?? panic("Sender has no PYUSD0 vault at ".concat(vaultData.storagePath.toString())) + self.vault <- senderVault.withdraw(amount: amount) + + // Set up receiver's PYUSD0 vault if not present + if rcvr.storage.borrow<&{FungibleToken.Vault}>(from: vaultData.storagePath) == nil { + rcvr.storage.save(<-vaultData.createEmptyVault(), to: vaultData.storagePath) + rcvr.capabilities.unpublish(vaultData.receiverPath) + rcvr.capabilities.unpublish(vaultData.metadataPath) + let receiverCap = rcvr.capabilities.storage.issue<&{FungibleToken.Vault}>(vaultData.storagePath) + let metadataCap = rcvr.capabilities.storage.issue<&{FungibleToken.Vault}>(vaultData.storagePath) + rcvr.capabilities.publish(receiverCap, at: vaultData.receiverPath) + rcvr.capabilities.publish(metadataCap, at: vaultData.metadataPath) + } + self.receiver = rcvr.storage.borrow<&{FungibleToken.Vault}>(from: vaultData.storagePath) + ?? panic("Could not borrow receiver PYUSD0 vault") + } + + execute { + self.receiver.deposit(from: <-self.vault) + log("Transferred ".concat(amount.toString()).concat(" PYUSD0 to receiver")) + } +} From fa041d866588d8185c63f3f50f8f0f419536b243 Mon Sep 17 00:00:00 2001 From: Alex <12097569+nialexsan@users.noreply.github.com> Date: Tue, 31 Mar 2026 11:03:08 -0400 Subject: [PATCH 62/72] clean up inject pyusd0 --- ...nject_pyusd0_as_fusdev_to_autobalancer.cdc | 21 ++---------- .../tests/transactions/transfer_pyusd0.cdc | 33 ++++++++----------- 2 files changed, 15 insertions(+), 39 deletions(-) diff --git a/cadence/tests/transactions/inject_pyusd0_as_fusdev_to_autobalancer.cdc b/cadence/tests/transactions/inject_pyusd0_as_fusdev_to_autobalancer.cdc index 3e107f6e..292a592d 100644 --- a/cadence/tests/transactions/inject_pyusd0_as_fusdev_to_autobalancer.cdc +++ b/cadence/tests/transactions/inject_pyusd0_as_fusdev_to_autobalancer.cdc @@ -1,9 +1,6 @@ import "FungibleToken" -import "FungibleTokenMetadataViews" -import "ViewResolver" import "FlowToken" import "EVM" -import "FlowEVMBridgeConfig" import "FungibleTokenConnectors" import "MorphoERC4626SwapConnectors" import "FlowYieldVaultsAutoBalancersV1" @@ -49,23 +46,9 @@ transaction( isReversed: false ) - // Locate the signer's PYUSD0 Cadence vault dynamically via FTVaultData. - let pyusd0EVM = EVM.addressFromString("0x99aF3EeA856556646C98c8B9b2548Fe815240750") - let pyusd0Type = FlowEVMBridgeConfig.getTypeAssociated(with: pyusd0EVM) - ?? panic("PYUSD0 EVM address not registered in bridge config") - 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") - let pyusd0Provider = signer.storage.borrow( - from: pyusd0VaultData.storagePath - ) ?? panic("No PYUSD0 vault found at ".concat(pyusd0VaultData.storagePath.toString())) + from: /storage/EVMVMBridgedToken_99af3eea856556646c98c8b9b2548fe815240750Vault + ) ?? panic("No PYUSD0 vault found") let pyusd0In <- pyusd0Provider.withdraw(amount: pyusd0Amount) diff --git a/cadence/tests/transactions/transfer_pyusd0.cdc b/cadence/tests/transactions/transfer_pyusd0.cdc index ed5ea036..6508f25a 100644 --- a/cadence/tests/transactions/transfer_pyusd0.cdc +++ b/cadence/tests/transactions/transfer_pyusd0.cdc @@ -18,32 +18,25 @@ transaction(amount: UFix64) { sender: auth(BorrowValue) &Account, rcvr: auth(Storage, IssueStorageCapabilityController, PublishCapability, UnpublishCapability) &Account ) { - let pyusd0Type = CompositeType("A.1e4aa0b87d10b141.EVMVMBridgedToken_99af3eea856556646c98c8b9b2548fe815240750.Vault")! - let viewResolver = getAccount(pyusd0Type.address!).contracts.borrow<&{ViewResolver}>(name: pyusd0Type.contractName!) - ?? panic("Could not borrow ViewResolver for PYUSD0") - let vaultData = viewResolver.resolveContractView( - resourceType: pyusd0Type, - viewType: Type() - ) as! FungibleTokenMetadataViews.FTVaultData? - ?? panic("Could not resolve FTVaultData for PYUSD0") + let storagePath = /storage/EVMVMBridgedToken_99af3eea856556646c98c8b9b2548fe815240750Vault - // Withdraw from sender - let senderVault = sender.storage.borrow( - from: vaultData.storagePath - ) ?? panic("Sender has no PYUSD0 vault at ".concat(vaultData.storagePath.toString())) - self.vault <- senderVault.withdraw(amount: amount) + self.vault <- (sender.storage.borrow(from: storagePath) + ?? panic("Sender has no PYUSD0 vault")).withdraw(amount: amount) // Set up receiver's PYUSD0 vault if not present - if rcvr.storage.borrow<&{FungibleToken.Vault}>(from: vaultData.storagePath) == nil { - rcvr.storage.save(<-vaultData.createEmptyVault(), to: vaultData.storagePath) + if rcvr.storage.borrow<&{FungibleToken.Vault}>(from: storagePath) == nil { + let pyusd0Type = CompositeType("A.1e4aa0b87d10b141.EVMVMBridgedToken_99af3eea856556646c98c8b9b2548fe815240750.Vault")! + let vaultData = getAccount(pyusd0Type.address!).contracts.borrow<&{ViewResolver}>(name: pyusd0Type.contractName!)! + .resolveContractView(resourceType: pyusd0Type, viewType: Type()) + as! FungibleTokenMetadataViews.FTVaultData? + ?? panic("Could not resolve FTVaultData for PYUSD0") + rcvr.storage.save(<-vaultData.createEmptyVault(), to: storagePath) rcvr.capabilities.unpublish(vaultData.receiverPath) rcvr.capabilities.unpublish(vaultData.metadataPath) - let receiverCap = rcvr.capabilities.storage.issue<&{FungibleToken.Vault}>(vaultData.storagePath) - let metadataCap = rcvr.capabilities.storage.issue<&{FungibleToken.Vault}>(vaultData.storagePath) - rcvr.capabilities.publish(receiverCap, at: vaultData.receiverPath) - rcvr.capabilities.publish(metadataCap, at: vaultData.metadataPath) + rcvr.capabilities.publish(rcvr.capabilities.storage.issue<&{FungibleToken.Vault}>(storagePath), at: vaultData.receiverPath) + rcvr.capabilities.publish(rcvr.capabilities.storage.issue<&{FungibleToken.Vault}>(storagePath), at: vaultData.metadataPath) } - self.receiver = rcvr.storage.borrow<&{FungibleToken.Vault}>(from: vaultData.storagePath) + self.receiver = rcvr.storage.borrow<&{FungibleToken.Vault}>(from: storagePath) ?? panic("Could not borrow receiver PYUSD0 vault") } From c17d4ed2052e9a4cd85cdb677ebc3ca3c897ef06 Mon Sep 17 00:00:00 2001 From: Alex <12097569+nialexsan@users.noreply.github.com> Date: Tue, 31 Mar 2026 11:11:56 -0400 Subject: [PATCH 63/72] update FlowALP ref --- lib/FlowALP | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/FlowALP b/lib/FlowALP index cded789d..aa10eff7 160000 --- a/lib/FlowALP +++ b/lib/FlowALP @@ -1 +1 @@ -Subproject commit cded789d07d9d3d92914036533d93c6b0a0417cd +Subproject commit aa10eff7bfd7a0e351b37ff9ef79a106003d66be From b86c0cf8303190cbe23120e936e92808efc3f8c4 Mon Sep 17 00:00:00 2001 From: Alex <12097569+nialexsan@users.noreply.github.com> Date: Tue, 31 Mar 2026 13:12:34 -0400 Subject: [PATCH 64/72] clean up --- .../contracts/FlowYieldVaultsStrategiesV2.cdc | 111 ++++++++-------- .../scripts/band-oracle/get_pyusd_price.cdc | 37 ------ ...wYieldVaultsStrategiesV2_syWFLOWv_test.cdc | 1 - ...flow_yield_vaults_backward_compat_test.cdc | 10 +- ...nject_pyusd0_as_fusdev_to_autobalancer.cdc | 2 +- .../band-oracle/get_pyusd_price.cdc | 36 ------ .../admin/upsert_more_erc4626_config.cdc | 31 +++-- local/setup_testnet_fork.sh | 121 ------------------ 8 files changed, 77 insertions(+), 272 deletions(-) delete mode 100644 cadence/scripts/band-oracle/get_pyusd_price.cdc delete mode 100644 cadence/transactions/band-oracle/get_pyusd_price.cdc delete mode 100755 local/setup_testnet_fork.sh diff --git a/cadence/contracts/FlowYieldVaultsStrategiesV2.cdc b/cadence/contracts/FlowYieldVaultsStrategiesV2.cdc index 6b0bb943..64647292 100644 --- a/cadence/contracts/FlowYieldVaultsStrategiesV2.cdc +++ b/cadence/contracts/FlowYieldVaultsStrategiesV2.cdc @@ -45,9 +45,7 @@ access(all) contract FlowYieldVaultsStrategiesV2 { /// Partitioned config map. Each key is a partition name; each value is a typed nested map keyed by /// strategy UniqueIdentifier ID (UInt64). Current partitions: /// "closedPositions" → {UInt64: Bool} - /// "syWFLOWvDebtTokenTypes" → {UInt64: Type} /// "moreERC4626Configs" → {Type: {Type: {Type: MoreERC4626CollateralConfig}}} - access(contract) let config: {String: AnyStruct} /// Canonical StoragePath where the StrategyComposerIssuer should be stored @@ -120,19 +118,10 @@ access(all) contract FlowYieldVaultsStrategiesV2 { } } - // @deprecated - /// 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. + /// @deprecated — no longer used. Retained for Cadence upgrade compatibility (structs cannot + /// be removed once deployed on-chain). 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( @@ -421,12 +410,30 @@ access(all) contract FlowYieldVaultsStrategiesV2 { // not consumed during debt repayment — and convert them directly to collateral. // The SwapSource inside closePosition only pulled what was needed to repay the debt; // any surplus shares are still held by the AutoBalancer and are recovered here. + // + // Use a MultiSwapper so the best available route is chosen: + // - Direct: FUSDEV → collateral via the stored yieldToCollateral AMM path (works + // even for 2-element paths where a direct yield↔collateral pool exists) + // - 2-hop: FUSDEV → PYUSD0 → collateral (via yieldToPyusd0 + debtToCollateral) let excessShares <- yieldTokenSource.withdrawAvailable(maxAmount: UFix64.max) if excessShares.balance > 0.0 { - let sharesToCollateral = SwapConnectors.SequentialSwapper( + let yieldToCollateralDirect = FlowYieldVaultsStrategiesV2._buildUniV3Swapper( + tokenPath: closeCollateralConfig.yieldToCollateralUniV3AddressPath, + feePath: closeCollateralConfig.yieldToCollateralUniV3FeePath, + inVault: closeTokens.yieldTokenType, + outVault: collateralType, + uniqueID: self.uniqueID! + ) + let yieldToCollateralViaDebt = SwapConnectors.SequentialSwapper( swappers: [yieldToPyusd0Swapper, debtToCollateralSwapper], uniqueID: self.copyID() ) + let sharesToCollateral = SwapConnectors.MultiSwapper( + inVault: closeTokens.yieldTokenType, + outVault: collateralType, + swappers: [yieldToCollateralDirect, yieldToCollateralViaDebt], + uniqueID: self.copyID() + ) let quote = sharesToCollateral.quoteOut(forProvided: excessShares.balance, reverse: false) if quote.outAmount > 0.0 { let extraCollateral <- sharesToCollateral.swap(quote: quote, inVault: <-excessShares) @@ -529,7 +536,11 @@ access(all) contract FlowYieldVaultsStrategiesV2 { ): UniswapV3SwapConnectors.Swapper { let yieldToCollPath = collateralConfig.yieldToCollateralUniV3AddressPath let yieldToCollFees = collateralConfig.yieldToCollateralUniV3FeePath - assert(yieldToCollPath.length >= 2, message: "yieldToCollateral path must have at least 2 elements") + // Requires at least 3 elements: [yield, PYUSD0 (debt/underlying), collateral, ...]. + // PYUSD0 must be at index 1 — it is the debt token and the derivation logic below + // assumes it as the starting point of the collateral→debt path. A direct yield↔collateral + // pool (2-element path) would omit PYUSD0 entirely and is structurally incompatible. + assert(yieldToCollPath.length >= 3, message: "yieldToCollateral path must have at least 3 elements [yield, PYUSD0, collateral] — a direct yield↔collateral pool is incompatible with FUSDEVStrategy debt routing") // Build reversed path: iterate yieldToCollPath from last down to index 1 (skip yield token at 0). // e.g. [FUSDEV, PYUSD0, WETH, WBTC] → [WBTC, WETH, PYUSD0] var collToDebtPath: [EVM.EVMAddress] = [] @@ -563,7 +574,11 @@ access(all) contract FlowYieldVaultsStrategiesV2 { ): UniswapV3SwapConnectors.Swapper { let path = collateralConfig.yieldToCollateralUniV3AddressPath let fees = collateralConfig.yieldToCollateralUniV3FeePath - assert(path.length >= 2, message: "yieldToCollateral path must have at least 2 elements") + // Requires at least 3 elements: [yield, PYUSD0 (debt/underlying), collateral, ...]. + // PYUSD0 must be at index 1 — it is the debt token and the derivation logic below + // assumes it as the starting point of the debt→collateral path. A direct yield↔collateral + // pool (2-element path) would omit PYUSD0 entirely and is structurally incompatible. + assert(path.length >= 3, message: "yieldToCollateral path must have at least 3 elements [yield, PYUSD0, collateral] — a direct yield↔collateral pool is incompatible with FUSDEVStrategy debt routing") // Skip the yield token at index 0; path[1..] starts at PYUSD0 (the underlying/debt token). var pyusd0ToCollPath: [EVM.EVMAddress] = [] var pyusd0ToCollFees: [UInt32] = [] @@ -619,6 +634,12 @@ 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. + /// NOTE: FUSDEVStrategy stores this flag in the contract-level "closedPositions" config + /// partition (via _isPositionClosed / _markPositionClosed) because FUSDEVStrategy was + /// already deployed on-chain when that tracking was introduced, and Cadence does not allow + /// adding fields to existing deployed resources. syWFLOWvStrategy was added after that + /// point and can therefore carry the flag as a plain resource field. access(self) var positionClosed: Bool init( @@ -651,14 +672,6 @@ access(all) contract FlowYieldVaultsStrategiesV2 { 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, @@ -847,7 +860,6 @@ access(all) contract FlowYieldVaultsStrategiesV2 { /// Executed when a Strategy is burned, cleaning up the Strategy's stored AutoBalancer and contract-level config entries access(contract) fun burnCallback() { FlowYieldVaultsAutoBalancersV1._cleanupAutoBalancer(id: self.id()!) - FlowYieldVaultsStrategiesV2._removeSyWFLOWvDebtTokenType(self.uniqueID?.id) } access(all) fun getComponentInfo(): DeFiActions.ComponentInfo { return DeFiActions.ComponentInfo( @@ -914,7 +926,7 @@ access(all) contract FlowYieldVaultsStrategiesV2 { } access(all) struct TokenBundle { - /// The MOET token type (the pool's borrowable token) + /// @deprecated — retained for Cadence upgrade compatibility; populated with placeholder values and not read. access(all) let moetTokenType: Type access(all) let moetTokenEVMAddress: EVM.EVMAddress @@ -996,14 +1008,11 @@ access(all) contract FlowYieldVaultsStrategiesV2 { return poolRef.getDefaultToken() } - /// Resolves the full token bundle for a strategy given the ERC4626 yield vault address. - /// moetTokenType/moetTokenEVMAddress are retained in the struct for upgrade compatibility - /// but are no longer used by active strategy code. + /// Resolves the token bundle for a strategy given the ERC4626 yield vault address. + /// moetTokenType/moetTokenEVMAddress are retained in TokenBundle for Cadence upgrade + /// compatibility (struct fields cannot be removed once deployed) but are no longer used — + /// the yield token address is passed as a placeholder to avoid unnecessary EVM lookups. access(self) fun _resolveTokenBundle(yieldTokenEVMAddress: EVM.EVMAddress): FlowYieldVaultsStrategiesV2.TokenBundle { - let moetTokenType = FlowYieldVaultsStrategiesV2._getPoolDefaultToken() - let moetTokenEVMAddress = FlowEVMBridgeConfig.getEVMAddressAssociated(with: moetTokenType) - ?? panic("Token Vault type \(moetTokenType.identifier) has not yet been registered with the VMbridge") - let yieldTokenType = FlowEVMBridgeConfig.getTypeAssociated(with: yieldTokenEVMAddress) ?? panic("Could not retrieve the VM Bridge associated Type for the yield token address \(yieldTokenEVMAddress.toString())") @@ -1013,8 +1022,8 @@ access(all) contract FlowYieldVaultsStrategiesV2 { ?? panic("Could not retrieve the VM Bridge associated Type for the ERC4626 underlying asset \(underlying4626AssetEVMAddress.toString())") return FlowYieldVaultsStrategiesV2.TokenBundle( - moetTokenType: moetTokenType, - moetTokenEVMAddress: moetTokenEVMAddress, + moetTokenType: yieldTokenType, // unused placeholder + moetTokenEVMAddress: yieldTokenEVMAddress, // unused placeholder yieldTokenType: yieldTokenType, yieldTokenEVMAddress: yieldTokenEVMAddress, underlying4626AssetType: underlying4626AssetType, @@ -1176,6 +1185,12 @@ access(all) contract FlowYieldVaultsStrategiesV2 { // FUSDEVStrategy: borrows PYUSD0 from the FlowALP position, deposits into FUSDEV // ----------------------------------------------------------------------- case Type<@FUSDEVStrategy>(): + // Reject PYUSD0 as collateral — it is the vault's underlying / debt token + assert( + collateralType != tokens.underlying4626AssetType, + message: "FUSDEVStrategy: PYUSD0 cannot be used as collateral — it is the vault's underlying asset" + ) + // Swappers: PYUSD0 (underlying/debt) <-> YIELD (FUSDEV) let debtToYieldSwapper = self._createDebtToYieldSwapper(tokens: tokens, uniqueID: uniqueID) let yieldToDebtSwapper = self._createYieldToDebtSwapper(tokens: tokens, uniqueID: uniqueID) @@ -1603,9 +1618,6 @@ access(all) contract FlowYieldVaultsStrategiesV2 { balancerIO.autoBalancer.setSink(positionDebtSwapSink, updateSinkID: true) balancerIO.autoBalancer.setSource(positionDebtSwapSource, updateSourceID: true) - // Store debtTokenType in contract-level config (resource field removed for upgrade compat) - FlowYieldVaultsStrategiesV2._setSyWFLOWvDebtTokenType(uniqueID.id, flowDebtTokenType) - return <-create syWFLOWvStrategy( id: uniqueID, collateralType: collateralType, @@ -1946,29 +1958,6 @@ access(all) contract FlowYieldVaultsStrategiesV2 { ) } - // --- "syWFLOWvDebtTokenTypes" partition --- - // Stores the debt token Type per syWFLOWvStrategy uniqueID. - // Kept in the contract-level config map so no new field is added to the deployed syWFLOWvStrategy resource. - - access(contract) view fun _getSyWFLOWvDebtTokenType(_ id: UInt64): Type? { - let partition = FlowYieldVaultsStrategiesV2.config["syWFLOWvDebtTokenTypes"] as! {UInt64: Type}? ?? {} - return partition[id] - } - - access(contract) fun _setSyWFLOWvDebtTokenType(_ id: UInt64, _ t: Type) { - var partition = FlowYieldVaultsStrategiesV2.config["syWFLOWvDebtTokenTypes"] as! {UInt64: Type}? ?? {} - partition[id] = t - FlowYieldVaultsStrategiesV2.config["syWFLOWvDebtTokenTypes"] = partition - } - - access(contract) fun _removeSyWFLOWvDebtTokenType(_ id: UInt64?) { - if let unwrappedID = id { - var partition = FlowYieldVaultsStrategiesV2.config["syWFLOWvDebtTokenTypes"] as! {UInt64: Type}? ?? {} - partition.remove(key: unwrappedID) - FlowYieldVaultsStrategiesV2.config["syWFLOWvDebtTokenTypes"] = partition - } - } - // --- "moreERC4626Configs" partition --- // Stores MoreERC4626CollateralConfig keyed by composer type → strategy type → collateral type. // Kept in the contract-level config map so no new field is added to the deployed StrategyComposerIssuer resource. diff --git a/cadence/scripts/band-oracle/get_pyusd_price.cdc b/cadence/scripts/band-oracle/get_pyusd_price.cdc deleted file mode 100644 index 8c741202..00000000 --- a/cadence/scripts/band-oracle/get_pyusd_price.cdc +++ /dev/null @@ -1,37 +0,0 @@ -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: "PYUSD", - quoteSymbol: "USD", - payment: <-payment - ) - - return priceData -} diff --git a/cadence/tests/FlowYieldVaultsStrategiesV2_syWFLOWv_test.cdc b/cadence/tests/FlowYieldVaultsStrategiesV2_syWFLOWv_test.cdc index dc737ff2..f876f1fb 100644 --- a/cadence/tests/FlowYieldVaultsStrategiesV2_syWFLOWv_test.cdc +++ b/cadence/tests/FlowYieldVaultsStrategiesV2_syWFLOWv_test.cdc @@ -64,7 +64,6 @@ 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" diff --git a/cadence/tests/fork/flow_yield_vaults_backward_compat_test.cdc b/cadence/tests/fork/flow_yield_vaults_backward_compat_test.cdc index b7bafa39..071ba13e 100644 --- a/cadence/tests/fork/flow_yield_vaults_backward_compat_test.cdc +++ b/cadence/tests/fork/flow_yield_vaults_backward_compat_test.cdc @@ -85,12 +85,10 @@ access(all) fun setup() { path: "../../contracts/FlowYieldVaultsSchedulerV1.cdc", arguments: [] ), - - // @TODO restore in strategies PR - // ContractSpec( - // path: "../../contracts/FlowYieldVaultsStrategiesV2.cdc", - // arguments: [univ3Factory, univ3Router, univ3Quoter] - // ), + ContractSpec( + path: "../../contracts/FlowYieldVaultsStrategiesV2.cdc", + arguments: [univ3Factory, univ3Router, univ3Quoter] + ), ContractSpec( path: "../../contracts/PMStrategiesV1.cdc", arguments: [univ3Factory, univ3Router, univ3Quoter] diff --git a/cadence/tests/transactions/inject_pyusd0_as_fusdev_to_autobalancer.cdc b/cadence/tests/transactions/inject_pyusd0_as_fusdev_to_autobalancer.cdc index 292a592d..44e34980 100644 --- a/cadence/tests/transactions/inject_pyusd0_as_fusdev_to_autobalancer.cdc +++ b/cadence/tests/transactions/inject_pyusd0_as_fusdev_to_autobalancer.cdc @@ -9,7 +9,7 @@ import "FlowYieldVaultsAutoBalancersV1" /// shares directly into the AutoBalancer for the given yield vault ID. /// /// This simulates accumulated yield in the AutoBalancer, producing a scenario where -/// the yield token (FUSDEV) balance is greater than the vault's outstanding MOET debt. +/// the yield token (FUSDEV) balance is greater than the vault's outstanding PYUSD0 debt. /// /// The signer must hold PYUSD0 in their Cadence vault and have a COA at /storage/evm. /// FLOW at /storage/flowTokenVault is used for bridge fees. diff --git a/cadence/transactions/band-oracle/get_pyusd_price.cdc b/cadence/transactions/band-oracle/get_pyusd_price.cdc deleted file mode 100644 index 2f0ec4bd..00000000 --- a/cadence/transactions/band-oracle/get_pyusd_price.cdc +++ /dev/null @@ -1,36 +0,0 @@ -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/cadence/transactions/flow-yield-vaults/admin/upsert_more_erc4626_config.cdc b/cadence/transactions/flow-yield-vaults/admin/upsert_more_erc4626_config.cdc index d906030d..e1d75bd2 100644 --- a/cadence/transactions/flow-yield-vaults/admin/upsert_more_erc4626_config.cdc +++ b/cadence/transactions/flow-yield-vaults/admin/upsert_more_erc4626_config.cdc @@ -26,13 +26,20 @@ transaction( debtToCollateralPath: [String], debtToCollateralFees: [UInt32] ) { + let issuer: auth(FlowYieldVaultsStrategiesV2.Configure) &FlowYieldVaultsStrategiesV2.StrategyComposerIssuer + let strategyType: Type + let tokenType: Type + let yieldTokenEVMAddr: EVM.EVMAddress + let yieldToUnderlyingAddressPath: [EVM.EVMAddress] + let debtToCollateralAddressPath: [EVM.EVMAddress] + prepare(acct: auth(Storage) &Account) { - let strategyType = CompositeType(strategyTypeIdentifier) + self.strategyType = CompositeType(strategyTypeIdentifier) ?? panic("Invalid strategyTypeIdentifier \(strategyTypeIdentifier)") - let tokenType = CompositeType(tokenTypeIdentifier) + self.tokenType = CompositeType(tokenTypeIdentifier) ?? panic("Invalid tokenTypeIdentifier \(tokenTypeIdentifier)") - let issuer = acct.storage.borrow< + self.issuer = acct.storage.borrow< auth(FlowYieldVaultsStrategiesV2.Configure) &FlowYieldVaultsStrategiesV2.StrategyComposerIssuer >(from: FlowYieldVaultsStrategiesV2.IssuerStoragePath) ?? panic("Missing StrategyComposerIssuer at IssuerStoragePath") @@ -43,13 +50,19 @@ transaction( return out } - issuer.addOrUpdateMoreERC4626CollateralConfig( - strategyType: strategyType, - collateralVaultType: tokenType, - yieldTokenEVMAddress: EVM.addressFromString(yieldTokenEVMAddress), - yieldToUnderlyingAddressPath: toEVM(yieldToUnderlyingPath), + self.yieldTokenEVMAddr = EVM.addressFromString(yieldTokenEVMAddress) + self.yieldToUnderlyingAddressPath = toEVM(yieldToUnderlyingPath) + self.debtToCollateralAddressPath = toEVM(debtToCollateralPath) + } + + execute { + self.issuer.addOrUpdateMoreERC4626CollateralConfig( + strategyType: self.strategyType, + collateralVaultType: self.tokenType, + yieldTokenEVMAddress: self.yieldTokenEVMAddr, + yieldToUnderlyingAddressPath: self.yieldToUnderlyingAddressPath, yieldToUnderlyingFeePath: yieldToUnderlyingFees, - debtToCollateralAddressPath: toEVM(debtToCollateralPath), + debtToCollateralAddressPath: self.debtToCollateralAddressPath, debtToCollateralFeePath: debtToCollateralFees ) } diff --git a/local/setup_testnet_fork.sh b/local/setup_testnet_fork.sh deleted file mode 100755 index 9c204a64..00000000 --- a/local/setup_testnet_fork.sh +++ /dev/null @@ -1,121 +0,0 @@ -#!/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 44a60c95bfb907c690902d4150889de8cf292fe9 Mon Sep 17 00:00:00 2001 From: Alex <12097569+nialexsan@users.noreply.github.com> Date: Tue, 31 Mar 2026 13:25:04 -0400 Subject: [PATCH 65/72] remove moet from setup testnet --- local/setup_testnet.sh | 23 +++++++++++------------ 1 file changed, 11 insertions(+), 12 deletions(-) diff --git a/local/setup_testnet.sh b/local/setup_testnet.sh index dd1f973a..0de795e6 100755 --- a/local/setup_testnet.sh +++ b/local/setup_testnet.sh @@ -119,34 +119,34 @@ flow transactions send ./cadence/transactions/flow-yield-vaults/admin/add_strate ## PYUSD0 Vault # WFLOW univ3 path and fees -# path: FUSDEV - MOET - WFLOW +# path: FUSDEV - PYUSD0 - WFLOW flow transactions send ./cadence/transactions/flow-yield-vaults/admin/upsert_strategy_config.cdc \ 'A.d2580caf2ef07c2f.FlowYieldVaultsStrategiesV2.FUSDEVStrategy' \ 'A.7e60df042a9c0868.FlowToken.Vault' \ "0x61b44D19486EE492449E83C1201581C754e9e1E1" \ - '["0x61b44D19486EE492449E83C1201581C754e9e1E1", "0xf622664Ba813e63947Cfa6c2E95E5c18F617E6C9", "0xd3bF53DAC106A0290B0483EcBC89d40FcC961f3e"]' \ + '["0x61b44D19486EE492449E83C1201581C754e9e1E1", "0xd7d43ab7b365f0d0789ae83f4385fa710ffdc98f", "0xd3bF53DAC106A0290B0483EcBC89d40FcC961f3e"]' \ '[100, 3000]' \ --network testnet \ --signer testnet-admin # WETH univ3 path and fees -# path: FUSDEV - MOET - WETH +# path: FUSDEV - PYUSD0 - WETH flow transactions send ./cadence/transactions/flow-yield-vaults/admin/upsert_strategy_config.cdc \ 'A.d2580caf2ef07c2f.FlowYieldVaultsStrategiesV2.FUSDEVStrategy' \ 'A.dfc20aee650fcbdf.EVMVMBridgedToken_059a77239dafa770977dd9f1e98632c3e4559848.Vault' \ "0x61b44D19486EE492449E83C1201581C754e9e1E1" \ - '["0x61b44D19486EE492449E83C1201581C754e9e1E1","0xf622664Ba813e63947Cfa6c2E95E5c18F617E6C9", "0x059A77239daFa770977DD9f1E98632C3E4559848"]' \ + '["0x61b44D19486EE492449E83C1201581C754e9e1E1","0xd7d43ab7b365f0d0789ae83f4385fa710ffdc98f", "0x059A77239daFa770977DD9f1E98632C3E4559848"]' \ '[100,3000]' \ --network testnet \ --signer testnet-admin # WBTC univ3 path and fees -# path: FUSDEV - MOET - WBTC +# path: FUSDEV - PYUSD0 - WBTC flow transactions send ./cadence/transactions/flow-yield-vaults/admin/upsert_strategy_config.cdc \ 'A.d2580caf2ef07c2f.FlowYieldVaultsStrategiesV2.FUSDEVStrategy' \ 'A.dfc20aee650fcbdf.EVMVMBridgedToken_208d09d2a6dd176e3e95b3f0de172a7471c5b2d6.Vault' \ "0x61b44D19486EE492449E83C1201581C754e9e1E1" \ - '["0x61b44D19486EE492449E83C1201581C754e9e1E1","0xf622664Ba813e63947Cfa6c2E95E5c18F617E6C9","0x208d09d2a6Dd176e3e95b3F0DE172A7471C5B2d6"]' \ + '["0x61b44D19486EE492449E83C1201581C754e9e1E1","0xd7d43ab7b365f0d0789ae83f4385fa710ffdc98f","0x208d09d2a6Dd176e3e95b3F0DE172A7471C5B2d6"]' \ '[100,3000]' \ --network testnet \ --signer testnet-admin @@ -175,7 +175,6 @@ flow transactions send ./cadence/transactions/flow-yield-vaults/admin/add_strate # UniswapV3 addresses (testnet): factory=0x92657b195e22b69E4779BBD09Fa3CD46F0CF8e39 # EVM tokens (testnet): # WFLOW: 0xd3bF53DAC106A0290B0483EcBC89d40FcC961f3e -# MOET: 0xf622664Ba813e63947Cfa6c2E95E5c18F617E6C9 (bridged; used as intermediate) # WBTC: 0x208d09d2a6Dd176e3e95b3F0DE172A7471C5B2d6 # WETH: 0x059A77239daFa770977DD9f1E98632C3E4559848 # PYUSD0: 0xd7d43ab7b365f0d0789ae83f4385fa710ffdc98f @@ -184,28 +183,28 @@ flow transactions send ./cadence/transactions/flow-yield-vaults/admin/add_strate # # 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) +# testnet uses PYUSD0 as the intermediate hop (mirrors testnet FUSDEVStrategy pool structure) -# WBTC collateral — syWFLOWv → WFLOW (fee 100), WFLOW → MOET → WBTC (fees 3000/3000) +# WBTC collateral — syWFLOWv → WFLOW (fee 100), WFLOW → PYUSD0 → WBTC (fees 3000/3000) flow transactions send ./cadence/transactions/flow-yield-vaults/admin/upsert_more_erc4626_config.cdc \ 'A.d2580caf2ef07c2f.FlowYieldVaultsStrategiesV2.syWFLOWvStrategy' \ 'A.dfc20aee650fcbdf.EVMVMBridgedToken_208d09d2a6dd176e3e95b3f0de172a7471c5b2d6.Vault' \ '0x' \ '["0x","0xd3bF53DAC106A0290B0483EcBC89d40FcC961f3e"]' \ '[100]' \ - '["0xd3bF53DAC106A0290B0483EcBC89d40FcC961f3e","0xf622664Ba813e63947Cfa6c2E95E5c18F617E6C9","0x208d09d2a6Dd176e3e95b3F0DE172A7471C5B2d6"]' \ + '["0xd3bF53DAC106A0290B0483EcBC89d40FcC961f3e","0xd7d43ab7b365f0d0789ae83f4385fa710ffdc98f","0x208d09d2a6Dd176e3e95b3F0DE172A7471C5B2d6"]' \ '[3000,3000]' \ --network testnet \ --signer testnet-admin -# WETH collateral — syWFLOWv → WFLOW (fee 100), WFLOW → MOET → WETH (fees 3000/3000) +# WETH collateral — syWFLOWv → WFLOW (fee 100), WFLOW → PYUSD0 → WETH (fees 3000/3000) flow transactions send ./cadence/transactions/flow-yield-vaults/admin/upsert_more_erc4626_config.cdc \ 'A.d2580caf2ef07c2f.FlowYieldVaultsStrategiesV2.syWFLOWvStrategy' \ 'A.dfc20aee650fcbdf.EVMVMBridgedToken_059a77239dafa770977dd9f1e98632c3e4559848.Vault' \ '0x' \ '["0x","0xd3bF53DAC106A0290B0483EcBC89d40FcC961f3e"]' \ '[100]' \ - '["0xd3bF53DAC106A0290B0483EcBC89d40FcC961f3e","0xf622664Ba813e63947Cfa6c2E95E5c18F617E6C9","0x059A77239daFa770977DD9f1E98632C3E4559848"]' \ + '["0xd3bF53DAC106A0290B0483EcBC89d40FcC961f3e","0xd7d43ab7b365f0d0789ae83f4385fa710ffdc98f","0x059A77239daFa770977DD9f1E98632C3E4559848"]' \ '[3000,3000]' \ --network testnet \ --signer testnet-admin From 55acf30a1041ce6f0ded0bb0d274ee716719ecd8 Mon Sep 17 00:00:00 2001 From: Alex <12097569+nialexsan@users.noreply.github.com> Date: Tue, 31 Mar 2026 16:27:13 -0400 Subject: [PATCH 66/72] remove unnecessary code --- .../contracts/FlowYieldVaultsStrategiesV2.cdc | 70 +++---------------- ...lowYieldVaultsStrategiesV2_FUSDEV_test.cdc | 11 +-- ...wYieldVaultsStrategiesV2_syWFLOWv_test.cdc | 15 ++-- 3 files changed, 23 insertions(+), 73 deletions(-) diff --git a/cadence/contracts/FlowYieldVaultsStrategiesV2.cdc b/cadence/contracts/FlowYieldVaultsStrategiesV2.cdc index 64647292..d2ff54a8 100644 --- a/cadence/contracts/FlowYieldVaultsStrategiesV2.cdc +++ b/cadence/contracts/FlowYieldVaultsStrategiesV2.cdc @@ -1632,60 +1632,8 @@ access(all) contract FlowYieldVaultsStrategiesV2 { access(all) entitlement Configure - access(self) - fun makeCollateralConfig( - yieldTokenEVMAddress: EVM.EVMAddress, - yieldToCollateralAddressPath: [EVM.EVMAddress], - yieldToCollateralFeePath: [UInt32] - ): CollateralConfig { - pre { - yieldToCollateralAddressPath.length > 1: - "Invalid Uniswap V3 swap path length" - yieldToCollateralFeePath.length == yieldToCollateralAddressPath.length - 1: - "Uniswap V3 fee path length must be path length - 1" - yieldToCollateralAddressPath[0].equals(yieldTokenEVMAddress): - "UniswapV3 swap path must start with yield token" - } - - return CollateralConfig( - yieldTokenEVMAddress: yieldTokenEVMAddress, - yieldToCollateralUniV3AddressPath: yieldToCollateralAddressPath, - yieldToCollateralUniV3FeePath: yieldToCollateralFeePath - ) - } - - 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" - debtToCollateralAddressPath[0].equals(yieldToUnderlyingAddressPath[yieldToUnderlyingAddressPath.length - 1]): - "debtToCollateral UniV3 path must start with the underlying asset (end of yieldToUnderlying path)" - } - 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 +/// 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 { @@ -1826,10 +1774,10 @@ access(all) contract FlowYieldVaultsStrategiesV2 { "Collateral type \(collateralVaultType.identifier) is not a FungibleToken.Vault" } - let base = FlowYieldVaultsStrategiesV2.makeCollateralConfig( + let base = CollateralConfig( yieldTokenEVMAddress: yieldTokenEVMAddress, - yieldToCollateralAddressPath: yieldToCollateralAddressPath, - yieldToCollateralFeePath: yieldToCollateralFeePath + yieldToCollateralUniV3AddressPath: yieldToCollateralAddressPath, + yieldToCollateralUniV3FeePath: yieldToCollateralFeePath ) self.upsertMorphoConfig(config: { strategyType: { collateralVaultType: base } }) } @@ -1850,12 +1798,12 @@ access(all) contract FlowYieldVaultsStrategiesV2 { "Collateral type \(collateralVaultType.identifier) is not a FungibleToken.Vault" } - let cfg = FlowYieldVaultsStrategiesV2.makeMoreERC4626CollateralConfig( + let cfg = MoreERC4626CollateralConfig( yieldTokenEVMAddress: yieldTokenEVMAddress, - yieldToUnderlyingAddressPath: yieldToUnderlyingAddressPath, - yieldToUnderlyingFeePath: yieldToUnderlyingFeePath, - debtToCollateralAddressPath: debtToCollateralAddressPath, - debtToCollateralFeePath: debtToCollateralFeePath + yieldToUnderlyingUniV3AddressPath: yieldToUnderlyingAddressPath, + yieldToUnderlyingUniV3FeePath: yieldToUnderlyingFeePath, + debtToCollateralUniV3AddressPath: debtToCollateralAddressPath, + debtToCollateralUniV3FeePath: debtToCollateralFeePath ) self.upsertMoreERC4626Config(config: { strategyType: { collateralVaultType: cfg } }) } diff --git a/cadence/tests/FlowYieldVaultsStrategiesV2_FUSDEV_test.cdc b/cadence/tests/FlowYieldVaultsStrategiesV2_FUSDEV_test.cdc index d506ca27..d27ae101 100644 --- a/cadence/tests/FlowYieldVaultsStrategiesV2_FUSDEV_test.cdc +++ b/cadence/tests/FlowYieldVaultsStrategiesV2_FUSDEV_test.cdc @@ -17,8 +17,8 @@ import "FlowYieldVaultsClosedBeta" /// assert the correct rejection. /// /// Strategy: -/// → FlowALP borrow MOET → swap MOET→PYUSD0 → ERC4626 deposit → FUSDEV (Morpho vault) -/// Close: FUSDEV → PYUSD0 (redeem) → MOET → repay FlowALP → returned to user +/// → FlowALP borrow PYUSD0 → ERC4626 deposit → FUSDEV (Morpho vault) +/// Close: FUSDEV → PYUSD0 (ERC4626 redeem) → repay FlowALP → returned to user /// /// Mainnet addresses: /// - Admin (FlowYieldVaults deployer): 0xb1d63873c3cc9f79 @@ -668,10 +668,11 @@ access(all) fun testCannotDepositWrongTokenToYieldVault() { /// 1. Open a FUSDEVStrategy vault with 0.001 WETH. /// 2. Swap 5 FLOW → PYUSD0 via EVM UniV3 WFLOW/PYUSD0 pool. /// 3. Convert PYUSD0 → FUSDEV and deposit directly into the AutoBalancer (public deposit()). -/// → AutoBalancer balance now exceeds what is needed to repay the MOET debt. +/// → AutoBalancer balance now exceeds what is needed to repay the PYUSD0 debt. /// 4. Close the vault. -/// → Step 9 of closePosition() drains the remaining FUSDEV, converts it -/// FUSDEV → MOET → collateral, and adds it to the returned collateral. +/// → Step 9 of closePosition() drains the remaining FUSDEV, converts it to +/// collateral via MultiSwapper (direct or FUSDEV → PYUSD0 → collateral), +/// and adds it to the returned collateral. /// 5. Verify collateral is returned and the user gains WETH from the excess. access(all) fun testCloseFUSDEVVaultWithExcessYieldTokens_WETH() { log("=== testCloseFUSDEVVaultWithExcessYieldTokens_WETH ===") diff --git a/cadence/tests/FlowYieldVaultsStrategiesV2_syWFLOWv_test.cdc b/cadence/tests/FlowYieldVaultsStrategiesV2_syWFLOWv_test.cdc index f876f1fb..b58191d1 100644 --- a/cadence/tests/FlowYieldVaultsStrategiesV2_syWFLOWv_test.cdc +++ b/cadence/tests/FlowYieldVaultsStrategiesV2_syWFLOWv_test.cdc @@ -400,9 +400,10 @@ access(all) fun testCreateSyWFLOWvYieldVault_PYUSD0() { 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 DID NOT happen by checking FlowALPv0.Deposited events: - // - There must be NO Deposited event with vaultType = MOET (pre-swapped collateral) - // - There must be a Deposited event with vaultType = PYUSD0 (should never reach FlowALP) + // Verify PYUSD0 was deposited directly into FlowALP as collateral (no intermediate token swap). + // syWFLOWvStrategy does not involve MOET at any point — PYUSD0 is deposited as-is. + // - There must be a Deposited event with vaultType = PYUSD0 (collateral deposited directly) + // - There must be NO Deposited event with vaultType = MOET (no pre-swap should occur) let depositedEvents = Test.eventsOfType(Type()) log("FlowALPv0.Deposited events: ".concat(depositedEvents.length.toString())) @@ -419,11 +420,11 @@ access(all) fun testCreateSyWFLOWvYieldVault_PYUSD0() { foundPyusd0Deposit = true } } - Test.assert(!foundMoetDeposit, - message: "Unxpected FlowALPv0.Deposited event with MOET — pre-swap did not deposit PYUSD0 into FlowALP") Test.assert(foundPyusd0Deposit, - message: "Expected FlowALPv0.Deposited event with PYUSD0 — pre-swap was bypassed") - log("Confirmed: FlowALP received PYUSD0 as collateral (MOET was NOT pre-swapped before FlowALP deposit)") + message: "Expected FlowALPv0.Deposited event with PYUSD0 — PYUSD0 collateral was not deposited into FlowALP") + Test.assert(!foundMoetDeposit, + message: "Unexpected FlowALPv0.Deposited event with MOET — syWFLOWvStrategy should not involve MOET") + log("Confirmed: FlowALP received PYUSD0 directly as collateral (no MOET involvement)") } access(all) fun testDepositToSyWFLOWvYieldVault_PYUSD0() { From 491e884221f6048309822af08aa5306f0ebefd11 Mon Sep 17 00:00:00 2001 From: Alex <12097569+nialexsan@users.noreply.github.com> Date: Tue, 31 Mar 2026 20:48:34 -0400 Subject: [PATCH 67/72] switch tests to use PYUSD and FLOW for excess yield --- ...lowYieldVaultsStrategiesV2_FUSDEV_test.cdc | 102 +++++++++--------- ...wYieldVaultsStrategiesV2_syWFLOWv_test.cdc | 90 ++++++++-------- 2 files changed, 97 insertions(+), 95 deletions(-) diff --git a/cadence/tests/FlowYieldVaultsStrategiesV2_FUSDEV_test.cdc b/cadence/tests/FlowYieldVaultsStrategiesV2_FUSDEV_test.cdc index d27ae101..656ec2c2 100644 --- a/cadence/tests/FlowYieldVaultsStrategiesV2_FUSDEV_test.cdc +++ b/cadence/tests/FlowYieldVaultsStrategiesV2_FUSDEV_test.cdc @@ -136,6 +136,14 @@ fun _wethBalance(_ user: Test.TestAccount): UFix64 { return (r.returnValue as? UFix64) ?? 0.0 } +/// Returns the native FLOW balance for the given account. +access(all) +fun _flowBalance(_ user: Test.TestAccount): UFix64 { + let r = _executeScript("../scripts/flow-yield-vaults/get_flow_balance.cdc", [user.address]) + Test.expect(r, Test.beSucceeded()) + return (r.returnValue as? UFix64) ?? 0.0 +} + /* --- Setup --- */ access(all) fun setup() { @@ -661,68 +669,63 @@ access(all) fun testCannotDepositWrongTokenToYieldVault() { Excess-yield tests ========================================================= */ -/// Opens a FUSDEVStrategy WETH vault, injects extra FUSDEV to create an excess scenario, -/// closes the vault, and verifies the resulting collateral return and excess burn behaviour. +/// Opens a FUSDEVStrategy WFLOW vault, injects extra FUSDEV to create an excess scenario, +/// closes the vault, and verifies that the excess is returned as FLOW to the user. +/// +/// Using FLOW as collateral makes the excess-return clearly visible: the user ends up with +/// more FLOW than they started with, because the injected FUSDEV shares are converted back +/// to FLOW (via FUSDEV → PYUSD0 → WFLOW) and added to the returned collateral. /// /// Scenario: -/// 1. Open a FUSDEVStrategy vault with 0.001 WETH. -/// 2. Swap 5 FLOW → PYUSD0 via EVM UniV3 WFLOW/PYUSD0 pool. -/// 3. Convert PYUSD0 → FUSDEV and deposit directly into the AutoBalancer (public deposit()). +/// 1. Open a FUSDEVStrategy vault with 10.0 FLOW. +/// 2. Convert 5.0 PYUSD0 → FUSDEV and deposit directly into the AutoBalancer. +/// (flowUser already holds PYUSD0 on mainnet — no transfer needed.) /// → AutoBalancer balance now exceeds what is needed to repay the PYUSD0 debt. -/// 4. Close the vault. +/// 3. Close the vault. /// → Step 9 of closePosition() drains the remaining FUSDEV, converts it to -/// collateral via MultiSwapper (direct or FUSDEV → PYUSD0 → collateral), -/// and adds it to the returned collateral. -/// 5. Verify collateral is returned and the user gains WETH from the excess. -access(all) fun testCloseFUSDEVVaultWithExcessYieldTokens_WETH() { - log("=== testCloseFUSDEVVaultWithExcessYieldTokens_WETH ===") +/// FLOW via MultiSwapper (FUSDEV → PYUSD0 → WFLOW), and adds it to the +/// returned collateral. +/// 4. Verify flowAfter > flowBefore: the user gained net FLOW from the excess. +access(all) fun testCloseFUSDEVVaultWithExcessYieldTokens_WFLOW() { + log("=== testCloseFUSDEVVaultWithExcessYieldTokens_WFLOW ===") - let wethBefore = _wethBalance(wethUser) - log("WETH balance before vault creation: ".concat(wethBefore.toString())) + let flowBefore = _flowBalance(flowUser) + log("FLOW balance before vault creation: ".concat(flowBefore.toString())) - let collateralAmount: UFix64 = 0.001 - log("Creating FUSDEVStrategy vault with ".concat(collateralAmount.toString()).concat(" WETH...")) + let collateralAmount: UFix64 = 10.0 + log("Creating FUSDEVStrategy vault with ".concat(collateralAmount.toString()).concat(" FLOW...")) let createResult = _executeTransactionFile( "../transactions/flow-yield-vaults/create_yield_vault.cdc", - [fusdEvStrategyIdentifier, wethVaultIdentifier, collateralAmount], - [wethUser] + [fusdEvStrategyIdentifier, flowVaultIdentifier, collateralAmount], + [flowUser] ) Test.expect(createResult, Test.beSucceeded()) - let vaultID = _latestVaultID(wethUser) + let vaultID = _latestVaultID(flowUser) log("Created vault ID: ".concat(vaultID.toString())) let vaultBalAfterCreate = _executeScript( "../scripts/flow-yield-vaults/get_yield_vault_balance.cdc", - [wethUser.address, vaultID] + [flowUser.address, vaultID] ) Test.expect(vaultBalAfterCreate, Test.beSucceeded()) let vaultBal = vaultBalAfterCreate.returnValue! as! UFix64? Test.assert(vaultBal != nil && vaultBal! > 0.0, message: "Expected positive vault balance after create, got: ".concat((vaultBal ?? 0.0).toString())) - log("Vault balance (WETH collateral value): ".concat(vaultBal!.toString())) + log("Vault balance (FLOW collateral value): ".concat(vaultBal!.toString())) let abBalBefore = _autoBalancerBalance(vaultID) Test.assert(abBalBefore != nil && abBalBefore! > 0.0, message: "Expected positive AutoBalancer balance after vault creation, got: ".concat((abBalBefore ?? 0.0).toString())) log("AutoBalancer FUSDEV balance before injection: ".concat(abBalBefore!.toString())) - // Transfer PYUSD0 from pyusd0Holder to wethUser (avoids EVM swap; pyusd0Holder has ~70k PYUSD0). + // flowUser already holds PYUSD0 on mainnet — inject directly without a transfer. let injectionPYUSD0Amount: UFix64 = 5.0 - log("Transferring ".concat(injectionPYUSD0Amount.toString()).concat(" PYUSD0 from pyusd0Holder to wethUser...")) - let transferResult = _executeTransactionFile( - "transactions/transfer_pyusd0.cdc", - [injectionPYUSD0Amount], - [pyusd0Holder, wethUser] - ) - Test.expect(transferResult, Test.beSucceeded()) - - // Inject PYUSD0 → FUSDEV into the AutoBalancer. log("Injecting ".concat(injectionPYUSD0Amount.toString()).concat(" PYUSD0 worth of FUSDEV into AutoBalancer...")) let injectResult = _executeTransactionFile( "transactions/inject_pyusd0_as_fusdev_to_autobalancer.cdc", [vaultID, fusdEvEVMAddress, injectionPYUSD0Amount], - [wethUser] + [flowUser] ) Test.expect(injectResult, Test.beSucceeded()) @@ -740,13 +743,13 @@ access(all) fun testCloseFUSDEVVaultWithExcessYieldTokens_WETH() { let closeResult = _executeTransactionFile( "../transactions/flow-yield-vaults/close_yield_vault.cdc", [vaultID], - [wethUser] + [flowUser] ) Test.expect(closeResult, Test.beSucceeded()) let vaultBalAfterClose = _executeScript( "../scripts/flow-yield-vaults/get_yield_vault_balance.cdc", - [wethUser.address, vaultID] + [flowUser.address, vaultID] ) Test.expect(vaultBalAfterClose, Test.beSucceeded()) Test.assert(vaultBalAfterClose.returnValue == nil, @@ -758,28 +761,21 @@ access(all) fun testCloseFUSDEVVaultWithExcessYieldTokens_WETH() { message: "AutoBalancer should be nil (burned) after vault close, but got: ".concat((abBalFinal ?? 0.0).toString())) log("AutoBalancer is nil after close — torn down during _cleanupAutoBalancer") - let wethAfter = _wethBalance(wethUser) - log("WETH balance after close: ".concat(wethAfter.toString())) - - let tolerance: UFix64 = collateralAmount * 0.05 - Test.assert( - wethAfter >= wethBefore - tolerance, - message: "User should have received ~".concat(collateralAmount.toString()) - .concat(" WETH back (minus swap fees). Before: ").concat(wethBefore.toString()) - .concat(", After: ").concat(wethAfter.toString()) - .concat(", Expected min: ").concat((wethBefore - tolerance).toString()) - ) + let flowAfter = _flowBalance(flowUser) + log("FLOW balance after close: ".concat(flowAfter.toString())) - // 5 PYUSD0 ≈ $5 at current prices ≈ 0.002 WETH — well above any fee loss on 0.001 WETH collateral. + // 5 PYUSD0 ≈ $5 at current prices — well above tx fees incurred during this test. + // The net gain should be clearly positive: excess FUSDEV → PYUSD0 → WFLOW adds more + // FLOW back than the transactions consume in fees. Test.assert( - wethAfter > wethBefore, - message: "User should have received MORE WETH than before (excess FUSDEV converted to collateral). " - .concat("Before: ").concat(wethBefore.toString()) - .concat(", After: ").concat(wethAfter.toString()) + flowAfter > flowBefore, + message: "User should have more FLOW than before (excess FUSDEV converted back to FLOW). " + .concat("Before: ").concat(flowBefore.toString()) + .concat(", After: ").concat(flowAfter.toString()) ) - let wethNet = wethAfter - wethBefore - log("Net WETH gain from excess FUSDEV conversion: ".concat(wethNet.toString()) - .concat(" WETH (injected ≈").concat(injectionPYUSD0Amount.toString()).concat(" PYUSD0 worth)")) + let flowNet = flowAfter - flowBefore + log("Net FLOW gain from excess FUSDEV conversion: ".concat(flowNet.toString()) + .concat(" FLOW (injected ~").concat(injectionPYUSD0Amount.toString()).concat(" PYUSD0 worth)")) - log("=== testCloseFUSDEVVaultWithExcessYieldTokens_WETH PASSED ===") + log("=== testCloseFUSDEVVaultWithExcessYieldTokens_WFLOW PASSED ===") } diff --git a/cadence/tests/FlowYieldVaultsStrategiesV2_syWFLOWv_test.cdc b/cadence/tests/FlowYieldVaultsStrategiesV2_syWFLOWv_test.cdc index b58191d1..0320362c 100644 --- a/cadence/tests/FlowYieldVaultsStrategiesV2_syWFLOWv_test.cdc +++ b/cadence/tests/FlowYieldVaultsStrategiesV2_syWFLOWv_test.cdc @@ -125,6 +125,14 @@ fun _wethBalance(_ user: Test.TestAccount): UFix64 { return (r.returnValue as? UFix64) ?? 0.0 } +/// Returns the PYUSD0 Cadence vault balance for the given account. +access(all) +fun _pyusd0Balance(_ user: Test.TestAccount): UFix64 { + let r = _executeScript("../scripts/tokens/get_vault_balance_by_type.cdc", [user.address, pyusd0VaultIdentifier]) + Test.expect(r, Test.beSucceeded()) + return (r.returnValue as? UFix64) ?? 0.0 +} + /* --- Setup --- */ access(all) fun setup() { @@ -641,57 +649,62 @@ access(all) fun testCannotDepositWrongTokenToYieldVault() { Excess-yield test ========================================================= */ -/// Opens a syWFLOWvStrategy WETH vault, injects extra syWFLOWv to create an excess scenario, -/// closes the vault, and verifies the resulting collateral return and excess conversion behaviour. +/// Opens a syWFLOWvStrategy PYUSD0 vault, injects extra syWFLOWv to create an excess scenario, +/// closes the vault, and verifies that the excess is returned as PYUSD0 to the user. +/// +/// Using PYUSD0 as collateral makes the excess-return clearly visible: the user ends up with +/// more PYUSD0 than they started with, because the injected syWFLOWv shares are converted back +/// to PYUSD0 (via syWFLOWv → FLOW → PYUSD0) and added to the returned collateral. /// /// Scenario: -/// 1. Open a syWFLOWvStrategy vault with 0.001 WETH. +/// 1. Open a syWFLOWvStrategy vault with 2.0 PYUSD0. /// 2. Convert 50 FLOW → syWFLOWv and deposit directly into the AutoBalancer. +/// (pyusd0User already holds FLOW on mainnet — no setup needed.) /// → AutoBalancer balance now exceeds what is needed to repay the FLOW debt. /// 3. Close the vault. -/// → Step 8 of closePosition() drains the remaining syWFLOWv, converts it -/// syWFLOWv → FLOW → WETH, and adds it to the returned collateral. -/// 4. Verify collateral is returned and the user gains WETH from the excess. -access(all) fun testCloseSyWFLOWvVaultWithExcessYieldTokens_WETH() { - log("=== testCloseSyWFLOWvVaultWithExcessYieldTokens_WETH ===") +/// → Step 8 of closePosition() drains the remaining syWFLOWv, converts it to +/// PYUSD0 (syWFLOWv → FLOW → PYUSD0), and adds it to the returned collateral. +/// 4. Verify pyusd0After > pyusd0Before: the user gained net PYUSD0 from the excess. +access(all) fun testCloseSyWFLOWvVaultWithExcessYieldTokens_PYUSD0() { + log("=== testCloseSyWFLOWvVaultWithExcessYieldTokens_PYUSD0 ===") - let wethBefore = _wethBalance(wethUser) - log("WETH balance before vault creation: ".concat(wethBefore.toString())) + let pyusd0Before = _pyusd0Balance(pyusd0User) + log("PYUSD0 balance before vault creation: ".concat(pyusd0Before.toString())) - let collateralAmount: UFix64 = 0.001 - log("Creating syWFLOWvStrategy vault with ".concat(collateralAmount.toString()).concat(" WETH...")) + let collateralAmount: UFix64 = 2.0 + log("Creating syWFLOWvStrategy vault with ".concat(collateralAmount.toString()).concat(" PYUSD0...")) let createResult = _executeTransactionFile( "../transactions/flow-yield-vaults/create_yield_vault.cdc", - [syWFLOWvStrategyIdentifier, wethVaultIdentifier, collateralAmount], - [wethUser] + [syWFLOWvStrategyIdentifier, pyusd0VaultIdentifier, collateralAmount], + [pyusd0User] ) Test.expect(createResult, Test.beSucceeded()) - let vaultID = _latestVaultID(wethUser) + let vaultID = _latestVaultID(pyusd0User) log("Created vault ID: ".concat(vaultID.toString())) let vaultBalAfterCreate = _executeScript( "../scripts/flow-yield-vaults/get_yield_vault_balance.cdc", - [wethUser.address, vaultID] + [pyusd0User.address, vaultID] ) Test.expect(vaultBalAfterCreate, Test.beSucceeded()) let vaultBal = vaultBalAfterCreate.returnValue! as! UFix64? Test.assert(vaultBal != nil && vaultBal! > 0.0, message: "Expected positive vault balance after create, got: ".concat((vaultBal ?? 0.0).toString())) - log("Vault balance (WETH collateral value): ".concat(vaultBal!.toString())) + log("Vault balance (PYUSD0 collateral value): ".concat(vaultBal!.toString())) let abBalBefore = _autoBalancerBalance(vaultID) Test.assert(abBalBefore != nil && abBalBefore! > 0.0, message: "Expected positive AutoBalancer balance after vault creation, got: ".concat((abBalBefore ?? 0.0).toString())) log("AutoBalancer syWFLOWv balance before injection: ".concat(abBalBefore!.toString())) - // Convert 50 FLOW → syWFLOWv and inject into the AutoBalancer. - let injectionFlowAmount: UFix64 = 50.0 + // pyusd0User holds FLOW on mainnet — inject directly without any setup. + let injectionFlowAmount: UFix64 = 10.0 log("Injecting ".concat(injectionFlowAmount.toString()).concat(" FLOW worth of syWFLOWv into AutoBalancer...")) let injectResult = _executeTransactionFile( "transactions/inject_flow_as_sywflowv_to_autobalancer.cdc", [vaultID, syWFLOWvEVMAddress, injectionFlowAmount], - [wethUser] + [pyusd0User] ) Test.expect(injectResult, Test.beSucceeded()) @@ -709,13 +722,13 @@ access(all) fun testCloseSyWFLOWvVaultWithExcessYieldTokens_WETH() { let closeResult = _executeTransactionFile( "../transactions/flow-yield-vaults/close_yield_vault.cdc", [vaultID], - [wethUser] + [pyusd0User] ) Test.expect(closeResult, Test.beSucceeded()) let vaultBalAfterClose = _executeScript( "../scripts/flow-yield-vaults/get_yield_vault_balance.cdc", - [wethUser.address, vaultID] + [pyusd0User.address, vaultID] ) Test.expect(vaultBalAfterClose, Test.beSucceeded()) Test.assert(vaultBalAfterClose.returnValue == nil, @@ -727,28 +740,21 @@ access(all) fun testCloseSyWFLOWvVaultWithExcessYieldTokens_WETH() { message: "AutoBalancer should be nil (burned) after vault close, but got: ".concat((abBalFinal ?? 0.0).toString())) log("AutoBalancer is nil after close — torn down during _cleanupAutoBalancer") - let wethAfter = _wethBalance(wethUser) - log("WETH balance after close: ".concat(wethAfter.toString())) - - let tolerance: UFix64 = collateralAmount * 0.05 - Test.assert( - wethAfter >= wethBefore - tolerance, - message: "User should have received ~".concat(collateralAmount.toString()) - .concat(" WETH back (minus swap fees). Before: ").concat(wethBefore.toString()) - .concat(", After: ").concat(wethAfter.toString()) - .concat(", Expected min: ").concat((wethBefore - tolerance).toString()) - ) + let pyusd0After = _pyusd0Balance(pyusd0User) + log("PYUSD0 balance after close: ".concat(pyusd0After.toString())) - // 50 FLOW ≈ $35–50 ≈ 0.012–0.017 WETH — well above any fee loss on 0.001 WETH collateral. + // 10 FLOW ≈ $0.3–0.5 at current prices — well above tx fees incurred during this test. + // The net gain should be clearly positive: excess syWFLOWv → FLOW → PYUSD0 adds more + // PYUSD0 back than the transactions consume in fees. Test.assert( - wethAfter > wethBefore, - message: "User should have received MORE WETH than before (excess syWFLOWv converted to collateral). " - .concat("Before: ").concat(wethBefore.toString()) - .concat(", After: ").concat(wethAfter.toString()) + pyusd0After > pyusd0Before, + message: "User should have more PYUSD0 than before (excess syWFLOWv converted back to PYUSD0). " + .concat("Before: ").concat(pyusd0Before.toString()) + .concat(", After: ").concat(pyusd0After.toString()) ) - let wethNet = wethAfter - wethBefore - log("Net WETH gain from excess syWFLOWv conversion: ".concat(wethNet.toString()) - .concat(" WETH (injected ≈").concat(injectionFlowAmount.toString()).concat(" FLOW worth)")) + let pyusd0Net = pyusd0After - pyusd0Before + log("Net PYUSD0 gain from excess syWFLOWv conversion: ".concat(pyusd0Net.toString()) + .concat(" PYUSD0 (injected ~").concat(injectionFlowAmount.toString()).concat(" FLOW worth)")) - log("=== testCloseSyWFLOWvVaultWithExcessYieldTokens_WETH PASSED ===") + log("=== testCloseSyWFLOWvVaultWithExcessYieldTokens_PYUSD0 PASSED ===") } From 4bf882b88d6a4d4123f0d1429c1da54cb3f6c1d8 Mon Sep 17 00:00:00 2001 From: Alex <12097569+nialexsan@users.noreply.github.com> Date: Tue, 31 Mar 2026 21:16:40 -0400 Subject: [PATCH 68/72] format --- ...lowYieldVaultsStrategiesV2_FUSDEV_test.cdc | 38 +++---- ...wYieldVaultsStrategiesV2_syWFLOWv_test.cdc | 100 +++++++++--------- 2 files changed, 65 insertions(+), 73 deletions(-) diff --git a/cadence/tests/FlowYieldVaultsStrategiesV2_FUSDEV_test.cdc b/cadence/tests/FlowYieldVaultsStrategiesV2_FUSDEV_test.cdc index 656ec2c2..969feeda 100644 --- a/cadence/tests/FlowYieldVaultsStrategiesV2_FUSDEV_test.cdc +++ b/cadence/tests/FlowYieldVaultsStrategiesV2_FUSDEV_test.cdc @@ -690,10 +690,10 @@ access(all) fun testCloseFUSDEVVaultWithExcessYieldTokens_WFLOW() { log("=== testCloseFUSDEVVaultWithExcessYieldTokens_WFLOW ===") let flowBefore = _flowBalance(flowUser) - log("FLOW balance before vault creation: ".concat(flowBefore.toString())) + log("FLOW balance before vault creation: \(flowBefore)") let collateralAmount: UFix64 = 10.0 - log("Creating FUSDEVStrategy vault with ".concat(collateralAmount.toString()).concat(" FLOW...")) + log("Creating FUSDEVStrategy vault with \(collateralAmount) FLOW...") let createResult = _executeTransactionFile( "../transactions/flow-yield-vaults/create_yield_vault.cdc", [fusdEvStrategyIdentifier, flowVaultIdentifier, collateralAmount], @@ -702,7 +702,7 @@ access(all) fun testCloseFUSDEVVaultWithExcessYieldTokens_WFLOW() { Test.expect(createResult, Test.beSucceeded()) let vaultID = _latestVaultID(flowUser) - log("Created vault ID: ".concat(vaultID.toString())) + log("Created vault ID: \(vaultID)") let vaultBalAfterCreate = _executeScript( "../scripts/flow-yield-vaults/get_yield_vault_balance.cdc", @@ -711,17 +711,17 @@ access(all) fun testCloseFUSDEVVaultWithExcessYieldTokens_WFLOW() { Test.expect(vaultBalAfterCreate, Test.beSucceeded()) let vaultBal = vaultBalAfterCreate.returnValue! as! UFix64? Test.assert(vaultBal != nil && vaultBal! > 0.0, - message: "Expected positive vault balance after create, got: ".concat((vaultBal ?? 0.0).toString())) - log("Vault balance (FLOW collateral value): ".concat(vaultBal!.toString())) + message: "Expected positive vault balance after create, got: \(vaultBal ?? 0.0)") + log("Vault balance (FLOW collateral value): \(vaultBal!)") let abBalBefore = _autoBalancerBalance(vaultID) Test.assert(abBalBefore != nil && abBalBefore! > 0.0, - message: "Expected positive AutoBalancer balance after vault creation, got: ".concat((abBalBefore ?? 0.0).toString())) - log("AutoBalancer FUSDEV balance before injection: ".concat(abBalBefore!.toString())) + message: "Expected positive AutoBalancer balance after vault creation, got: \(abBalBefore ?? 0.0)") + log("AutoBalancer FUSDEV balance before injection: \(abBalBefore!)") // flowUser already holds PYUSD0 on mainnet — inject directly without a transfer. let injectionPYUSD0Amount: UFix64 = 5.0 - log("Injecting ".concat(injectionPYUSD0Amount.toString()).concat(" PYUSD0 worth of FUSDEV into AutoBalancer...")) + log("Injecting \(injectionPYUSD0Amount) PYUSD0 worth of FUSDEV into AutoBalancer...") let injectResult = _executeTransactionFile( "transactions/inject_pyusd0_as_fusdev_to_autobalancer.cdc", [vaultID, fusdEvEVMAddress, injectionPYUSD0Amount], @@ -733,13 +733,12 @@ access(all) fun testCloseFUSDEVVaultWithExcessYieldTokens_WFLOW() { Test.assert(abBalAfter != nil, message: "AutoBalancer should still exist after injection") Test.assert(abBalAfter! > abBalBefore!, - message: "AutoBalancer FUSDEV balance should have increased after injection. Before: " - .concat(abBalBefore!.toString()).concat(" After: ").concat(abBalAfter!.toString())) + message: "AutoBalancer FUSDEV balance should have increased after injection. Before: \(abBalBefore!) After: \(abBalAfter!)") let injectedShares = abBalAfter! - abBalBefore! - log("AutoBalancer FUSDEV balance after injection: ".concat(abBalAfter!.toString())) - log("Injected ".concat(injectedShares.toString()).concat(" FUSDEV shares (excess over original debt coverage)")) + log("AutoBalancer FUSDEV balance after injection: \(abBalAfter!)") + log("Injected \(injectedShares) FUSDEV shares (excess over original debt coverage)") - log("Closing vault ".concat(vaultID.toString()).concat("...")) + log("Closing vault \(vaultID)...") let closeResult = _executeTransactionFile( "../transactions/flow-yield-vaults/close_yield_vault.cdc", [vaultID], @@ -753,29 +752,26 @@ access(all) fun testCloseFUSDEVVaultWithExcessYieldTokens_WFLOW() { ) Test.expect(vaultBalAfterClose, Test.beSucceeded()) Test.assert(vaultBalAfterClose.returnValue == nil, - message: "Vault ".concat(vaultID.toString()).concat(" should not exist after close")) + message: "Vault \(vaultID) should not exist after close") log("Vault no longer exists — close confirmed") let abBalFinal = _autoBalancerBalance(vaultID) Test.assert(abBalFinal == nil, - message: "AutoBalancer should be nil (burned) after vault close, but got: ".concat((abBalFinal ?? 0.0).toString())) + message: "AutoBalancer should be nil (burned) after vault close, but got: \(abBalFinal ?? 0.0)") log("AutoBalancer is nil after close — torn down during _cleanupAutoBalancer") let flowAfter = _flowBalance(flowUser) - log("FLOW balance after close: ".concat(flowAfter.toString())) + log("FLOW balance after close: \(flowAfter)") // 5 PYUSD0 ≈ $5 at current prices — well above tx fees incurred during this test. // The net gain should be clearly positive: excess FUSDEV → PYUSD0 → WFLOW adds more // FLOW back than the transactions consume in fees. Test.assert( flowAfter > flowBefore, - message: "User should have more FLOW than before (excess FUSDEV converted back to FLOW). " - .concat("Before: ").concat(flowBefore.toString()) - .concat(", After: ").concat(flowAfter.toString()) + message: "User should have more FLOW than before (excess FUSDEV converted back to FLOW). Before: \(flowBefore), After: \(flowAfter)" ) let flowNet = flowAfter - flowBefore - log("Net FLOW gain from excess FUSDEV conversion: ".concat(flowNet.toString()) - .concat(" FLOW (injected ~").concat(injectionPYUSD0Amount.toString()).concat(" PYUSD0 worth)")) + log("Net FLOW gain from excess FUSDEV conversion: \(flowNet) FLOW (injected ~\(injectionPYUSD0Amount) PYUSD0 worth)") log("=== testCloseFUSDEVVaultWithExcessYieldTokens_WFLOW PASSED ===") } diff --git a/cadence/tests/FlowYieldVaultsStrategiesV2_syWFLOWv_test.cdc b/cadence/tests/FlowYieldVaultsStrategiesV2_syWFLOWv_test.cdc index 0320362c..cb03af93 100644 --- a/cadence/tests/FlowYieldVaultsStrategiesV2_syWFLOWv_test.cdc +++ b/cadence/tests/FlowYieldVaultsStrategiesV2_syWFLOWv_test.cdc @@ -104,7 +104,7 @@ 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())) + Test.assert(ids != nil && ids!.length > 0, message: "Expected at least one yield vault for \(user.address)") return ids![ids!.length - 1] } @@ -400,27 +400,27 @@ access(all) fun testCreateSyWFLOWvYieldVault_PYUSD0() { Test.expect(result, Test.beSucceeded()) pyusd0VaultID = _latestVaultID(pyusd0User) - log("Created PYUSD0 vault ID: ".concat(pyusd0VaultID.toString())) + log("Created PYUSD0 vault ID: \(pyusd0VaultID)") let bal = _executeScript("../scripts/flow-yield-vaults/get_yield_vault_balance.cdc", [pyusd0User.address, pyusd0VaultID]) Test.expect(bal, Test.beSucceeded()) let balance = bal.returnValue! as! UFix64? Test.assert(balance != nil && balance! > 0.0, message: "Expected positive balance after create (PYUSD0)") - log("PYUSD0 vault balance after create: ".concat(balance!.toString())) + log("PYUSD0 vault balance after create: \(balance!)") // Verify PYUSD0 was deposited directly into FlowALP as collateral (no intermediate token swap). // syWFLOWvStrategy does not involve MOET at any point — PYUSD0 is deposited as-is. // - There must be a Deposited event with vaultType = PYUSD0 (collateral deposited directly) // - There must be NO Deposited event with vaultType = MOET (no pre-swap should occur) let depositedEvents = Test.eventsOfType(Type()) - log("FlowALPv0.Deposited events: ".concat(depositedEvents.length.toString())) + log("FlowALPv0.Deposited events: \(depositedEvents.length)") let moetTypeID = "A.6b00ff876c299c61.MOET.Vault" var foundMoetDeposit = false var foundPyusd0Deposit = false for e in depositedEvents { let ev = e as! FlowALPv0.Deposited - log(" Deposited: vaultType=".concat(ev.vaultType.identifier).concat(" amount=").concat(ev.amount.toString())) + log(" Deposited: vaultType=\(ev.vaultType.identifier) amount=\(ev.amount)") if ev.vaultType.identifier == moetTypeID { foundMoetDeposit = true } @@ -438,34 +438,34 @@ access(all) fun testCreateSyWFLOWvYieldVault_PYUSD0() { 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("...")) + log("Depositing 0.5 PYUSD0 to vault \(pyusd0VaultID)...") Test.expect( _executeTransactionFile("../transactions/flow-yield-vaults/deposit_to_yield_vault.cdc", [pyusd0VaultID, depositAmount], [pyusd0User]), Test.beSucceeded() ) let after = (_executeScript("../scripts/flow-yield-vaults/get_yield_vault_balance.cdc", [pyusd0User.address, pyusd0VaultID]).returnValue! as! UFix64?)! Test.assert(equalAmounts(a: after, b: before + depositAmount, tolerance: 0.01), - message: "PYUSD0 deposit: expected ~".concat((before + depositAmount).toString()).concat(", got ").concat(after.toString())) - log("PYUSD0 vault balance after deposit: ".concat(after.toString())) + message: "PYUSD0 deposit: expected ~\(before + depositAmount), got \(after)") + log("PYUSD0 vault balance after deposit: \(after)") } access(all) fun testWithdrawFromSyWFLOWvYieldVault_PYUSD0() { let before = (_executeScript("../scripts/flow-yield-vaults/get_yield_vault_balance.cdc", [pyusd0User.address, pyusd0VaultID]).returnValue! as! UFix64?) ?? 0.0 let withdrawAmount: UFix64 = 0.3 - log("Withdrawing 0.3 PYUSD0 from vault ".concat(pyusd0VaultID.toString()).concat("...")) + log("Withdrawing 0.3 PYUSD0 from vault \(pyusd0VaultID)...") Test.expect( _executeTransactionFile("../transactions/flow-yield-vaults/withdraw_from_yield_vault.cdc", [pyusd0VaultID, withdrawAmount], [pyusd0User]), Test.beSucceeded() ) let after = (_executeScript("../scripts/flow-yield-vaults/get_yield_vault_balance.cdc", [pyusd0User.address, pyusd0VaultID]).returnValue! as! UFix64?)! Test.assert(equalAmounts(a: after, b: before - withdrawAmount, tolerance: 0.01), - message: "PYUSD0 withdraw: expected ~".concat((before - withdrawAmount).toString()).concat(", got ").concat(after.toString())) - log("PYUSD0 vault balance after withdrawal: ".concat(after.toString())) + message: "PYUSD0 withdraw: expected ~\(before - withdrawAmount), got \(after)") + log("PYUSD0 vault balance after withdrawal: \(after)") } access(all) fun testCloseSyWFLOWvYieldVault_PYUSD0() { let vaultBalBefore = (_executeScript("../scripts/flow-yield-vaults/get_yield_vault_balance.cdc", [pyusd0User.address, pyusd0VaultID]).returnValue! as! UFix64?) ?? 0.0 - log("Closing PYUSD0 vault ".concat(pyusd0VaultID.toString()).concat(" (balance: ").concat(vaultBalBefore.toString()).concat(")...")) + log("Closing PYUSD0 vault \(pyusd0VaultID) (balance: \(vaultBalBefore))...") Test.expect( _executeTransactionFile("../transactions/flow-yield-vaults/close_yield_vault.cdc", [pyusd0VaultID], [pyusd0User]), Test.beSucceeded() @@ -490,46 +490,46 @@ access(all) fun testCreateSyWFLOWvYieldVault_WBTC() { Test.expect(result, Test.beSucceeded()) wbtcVaultID = _latestVaultID(wbtcUser) - log("Created WBTC vault ID: ".concat(wbtcVaultID.toString())) + log("Created WBTC vault ID: \(wbtcVaultID)") let bal = _executeScript("../scripts/flow-yield-vaults/get_yield_vault_balance.cdc", [wbtcUser.address, wbtcVaultID]) Test.expect(bal, Test.beSucceeded()) let balance = bal.returnValue! as! UFix64? Test.assert(balance != nil && balance! > 0.0, message: "Expected positive balance after create (WBTC)") - log("WBTC vault balance after create: ".concat(balance!.toString())) + log("WBTC vault balance after create: \(balance!)") } access(all) fun testDepositToSyWFLOWvYieldVault_WBTC() { let before = (_executeScript("../scripts/flow-yield-vaults/get_yield_vault_balance.cdc", [wbtcUser.address, wbtcVaultID]).returnValue! as! UFix64?) ?? 0.0 let depositAmount: UFix64 = 0.00005 - log("Depositing 0.00005 WBTC to vault ".concat(wbtcVaultID.toString()).concat("...")) + log("Depositing 0.00005 WBTC to vault \(wbtcVaultID)...") Test.expect( _executeTransactionFile("../transactions/flow-yield-vaults/deposit_to_yield_vault.cdc", [wbtcVaultID, depositAmount], [wbtcUser]), Test.beSucceeded() ) let after = (_executeScript("../scripts/flow-yield-vaults/get_yield_vault_balance.cdc", [wbtcUser.address, wbtcVaultID]).returnValue! as! UFix64?)! Test.assert(equalAmounts(a: after, b: before + depositAmount, tolerance: 0.000005), - message: "WBTC deposit: expected ~".concat((before + depositAmount).toString()).concat(", got ").concat(after.toString())) - log("WBTC vault balance after deposit: ".concat(after.toString())) + message: "WBTC deposit: expected ~\(before + depositAmount), got \(after)") + log("WBTC vault balance after deposit: \(after)") } access(all) fun testWithdrawFromSyWFLOWvYieldVault_WBTC() { let before = (_executeScript("../scripts/flow-yield-vaults/get_yield_vault_balance.cdc", [wbtcUser.address, wbtcVaultID]).returnValue! as! UFix64?) ?? 0.0 let withdrawAmount: UFix64 = 0.00003 - log("Withdrawing 0.00003 WBTC from vault ".concat(wbtcVaultID.toString()).concat("...")) + log("Withdrawing 0.00003 WBTC from vault \(wbtcVaultID)...") Test.expect( _executeTransactionFile("../transactions/flow-yield-vaults/withdraw_from_yield_vault.cdc", [wbtcVaultID, withdrawAmount], [wbtcUser]), Test.beSucceeded() ) let after = (_executeScript("../scripts/flow-yield-vaults/get_yield_vault_balance.cdc", [wbtcUser.address, wbtcVaultID]).returnValue! as! UFix64?)! Test.assert(equalAmounts(a: after, b: before - withdrawAmount, tolerance: 0.000005), - message: "WBTC withdraw: expected ~".concat((before - withdrawAmount).toString()).concat(", got ").concat(after.toString())) - log("WBTC vault balance after withdrawal: ".concat(after.toString())) + message: "WBTC withdraw: expected ~\(before - withdrawAmount), got \(after)") + log("WBTC vault balance after withdrawal: \(after)") } access(all) fun testCloseSyWFLOWvYieldVault_WBTC() { let vaultBalBefore = (_executeScript("../scripts/flow-yield-vaults/get_yield_vault_balance.cdc", [wbtcUser.address, wbtcVaultID]).returnValue! as! UFix64?) ?? 0.0 - log("Closing WBTC vault ".concat(wbtcVaultID.toString()).concat(" (balance: ").concat(vaultBalBefore.toString()).concat(")...")) + log("Closing WBTC vault \(wbtcVaultID) (balance: \(vaultBalBefore))...") Test.expect( _executeTransactionFile("../transactions/flow-yield-vaults/close_yield_vault.cdc", [wbtcVaultID], [wbtcUser]), Test.beSucceeded() @@ -554,46 +554,46 @@ access(all) fun testCreateSyWFLOWvYieldVault_WETH() { Test.expect(result, Test.beSucceeded()) wethVaultID = _latestVaultID(wethUser) - log("Created WETH vault ID: ".concat(wethVaultID.toString())) + log("Created WETH vault ID: \(wethVaultID)") let bal = _executeScript("../scripts/flow-yield-vaults/get_yield_vault_balance.cdc", [wethUser.address, wethVaultID]) Test.expect(bal, Test.beSucceeded()) let balance = bal.returnValue! as! UFix64? Test.assert(balance != nil && balance! > 0.0, message: "Expected positive balance after create (WETH)") - log("WETH vault balance after create: ".concat(balance!.toString())) + log("WETH vault balance after create: \(balance!)") } access(all) fun testDepositToSyWFLOWvYieldVault_WETH() { let before = (_executeScript("../scripts/flow-yield-vaults/get_yield_vault_balance.cdc", [wethUser.address, wethVaultID]).returnValue! as! UFix64?) ?? 0.0 let depositAmount: UFix64 = 0.0005 - log("Depositing 0.0005 WETH to vault ".concat(wethVaultID.toString()).concat("...")) + log("Depositing 0.0005 WETH to vault \(wethVaultID)...") Test.expect( _executeTransactionFile("../transactions/flow-yield-vaults/deposit_to_yield_vault.cdc", [wethVaultID, depositAmount], [wethUser]), Test.beSucceeded() ) let after = (_executeScript("../scripts/flow-yield-vaults/get_yield_vault_balance.cdc", [wethUser.address, wethVaultID]).returnValue! as! UFix64?)! Test.assert(equalAmounts(a: after, b: before + depositAmount, tolerance: 0.00005), - message: "WETH deposit: expected ~".concat((before + depositAmount).toString()).concat(", got ").concat(after.toString())) - log("WETH vault balance after deposit: ".concat(after.toString())) + message: "WETH deposit: expected ~\(before + depositAmount), got \(after)") + log("WETH vault balance after deposit: \(after)") } access(all) fun testWithdrawFromSyWFLOWvYieldVault_WETH() { let before = (_executeScript("../scripts/flow-yield-vaults/get_yield_vault_balance.cdc", [wethUser.address, wethVaultID]).returnValue! as! UFix64?) ?? 0.0 let withdrawAmount: UFix64 = 0.0003 - log("Withdrawing 0.0003 WETH from vault ".concat(wethVaultID.toString()).concat("...")) + log("Withdrawing 0.0003 WETH from vault \(wethVaultID)...") Test.expect( _executeTransactionFile("../transactions/flow-yield-vaults/withdraw_from_yield_vault.cdc", [wethVaultID, withdrawAmount], [wethUser]), Test.beSucceeded() ) let after = (_executeScript("../scripts/flow-yield-vaults/get_yield_vault_balance.cdc", [wethUser.address, wethVaultID]).returnValue! as! UFix64?)! Test.assert(equalAmounts(a: after, b: before - withdrawAmount, tolerance: 0.00005), - message: "WETH withdraw: expected ~".concat((before - withdrawAmount).toString()).concat(", got ").concat(after.toString())) - log("WETH vault balance after withdrawal: ".concat(after.toString())) + message: "WETH withdraw: expected ~\(before - withdrawAmount), got \(after)") + log("WETH vault balance after withdrawal: \(after)") } access(all) fun testCloseSyWFLOWvYieldVault_WETH() { let vaultBalBefore = (_executeScript("../scripts/flow-yield-vaults/get_yield_vault_balance.cdc", [wethUser.address, wethVaultID]).returnValue! as! UFix64?) ?? 0.0 - log("Closing WETH vault ".concat(wethVaultID.toString()).concat(" (balance: ").concat(vaultBalBefore.toString()).concat(")...")) + log("Closing WETH vault \(wethVaultID) (balance: \(vaultBalBefore))...") Test.expect( _executeTransactionFile("../transactions/flow-yield-vaults/close_yield_vault.cdc", [wethVaultID], [wethUser]), Test.beSucceeded() @@ -633,7 +633,7 @@ access(all) fun testCannotDepositWrongTokenToYieldVault() { ) 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...")) + log("Created WETH vault ID: \(freshWethVaultID) — now attempting to deposit WBTC into it...") // Attempt to deposit WBTC (wrong type) into the WETH vault — must fail let depositResult = _executeTransactionFile( @@ -669,10 +669,10 @@ access(all) fun testCloseSyWFLOWvVaultWithExcessYieldTokens_PYUSD0() { log("=== testCloseSyWFLOWvVaultWithExcessYieldTokens_PYUSD0 ===") let pyusd0Before = _pyusd0Balance(pyusd0User) - log("PYUSD0 balance before vault creation: ".concat(pyusd0Before.toString())) + log("PYUSD0 balance before vault creation: \(pyusd0Before)") let collateralAmount: UFix64 = 2.0 - log("Creating syWFLOWvStrategy vault with ".concat(collateralAmount.toString()).concat(" PYUSD0...")) + log("Creating syWFLOWvStrategy vault with \(collateralAmount) PYUSD0...") let createResult = _executeTransactionFile( "../transactions/flow-yield-vaults/create_yield_vault.cdc", [syWFLOWvStrategyIdentifier, pyusd0VaultIdentifier, collateralAmount], @@ -681,7 +681,7 @@ access(all) fun testCloseSyWFLOWvVaultWithExcessYieldTokens_PYUSD0() { Test.expect(createResult, Test.beSucceeded()) let vaultID = _latestVaultID(pyusd0User) - log("Created vault ID: ".concat(vaultID.toString())) + log("Created vault ID: \(vaultID)") let vaultBalAfterCreate = _executeScript( "../scripts/flow-yield-vaults/get_yield_vault_balance.cdc", @@ -690,17 +690,17 @@ access(all) fun testCloseSyWFLOWvVaultWithExcessYieldTokens_PYUSD0() { Test.expect(vaultBalAfterCreate, Test.beSucceeded()) let vaultBal = vaultBalAfterCreate.returnValue! as! UFix64? Test.assert(vaultBal != nil && vaultBal! > 0.0, - message: "Expected positive vault balance after create, got: ".concat((vaultBal ?? 0.0).toString())) - log("Vault balance (PYUSD0 collateral value): ".concat(vaultBal!.toString())) + message: "Expected positive vault balance after create, got: \(vaultBal ?? 0.0)") + log("Vault balance (PYUSD0 collateral value): \(vaultBal!)") let abBalBefore = _autoBalancerBalance(vaultID) Test.assert(abBalBefore != nil && abBalBefore! > 0.0, - message: "Expected positive AutoBalancer balance after vault creation, got: ".concat((abBalBefore ?? 0.0).toString())) - log("AutoBalancer syWFLOWv balance before injection: ".concat(abBalBefore!.toString())) + message: "Expected positive AutoBalancer balance after vault creation, got: \(abBalBefore ?? 0.0)") + log("AutoBalancer syWFLOWv balance before injection: \(abBalBefore!)") // pyusd0User holds FLOW on mainnet — inject directly without any setup. let injectionFlowAmount: UFix64 = 10.0 - log("Injecting ".concat(injectionFlowAmount.toString()).concat(" FLOW worth of syWFLOWv into AutoBalancer...")) + log("Injecting \(injectionFlowAmount) FLOW worth of syWFLOWv into AutoBalancer...") let injectResult = _executeTransactionFile( "transactions/inject_flow_as_sywflowv_to_autobalancer.cdc", [vaultID, syWFLOWvEVMAddress, injectionFlowAmount], @@ -712,13 +712,12 @@ access(all) fun testCloseSyWFLOWvVaultWithExcessYieldTokens_PYUSD0() { Test.assert(abBalAfter != nil, message: "AutoBalancer should still exist after injection") Test.assert(abBalAfter! > abBalBefore!, - message: "AutoBalancer balance should have increased after injection. Before: " - .concat(abBalBefore!.toString()).concat(" After: ").concat(abBalAfter!.toString())) + message: "AutoBalancer balance should have increased after injection. Before: \(abBalBefore!) After: \(abBalAfter!)") let injectedShares = abBalAfter! - abBalBefore! - log("AutoBalancer syWFLOWv balance after injection: ".concat(abBalAfter!.toString())) - log("Injected ".concat(injectedShares.toString()).concat(" syWFLOWv shares (excess over original debt coverage)")) + log("AutoBalancer syWFLOWv balance after injection: \(abBalAfter!)") + log("Injected \(injectedShares) syWFLOWv shares (excess over original debt coverage)") - log("Closing vault ".concat(vaultID.toString()).concat("...")) + log("Closing vault \(vaultID)...") let closeResult = _executeTransactionFile( "../transactions/flow-yield-vaults/close_yield_vault.cdc", [vaultID], @@ -732,29 +731,26 @@ access(all) fun testCloseSyWFLOWvVaultWithExcessYieldTokens_PYUSD0() { ) Test.expect(vaultBalAfterClose, Test.beSucceeded()) Test.assert(vaultBalAfterClose.returnValue == nil, - message: "Vault ".concat(vaultID.toString()).concat(" should not exist after close")) + message: "Vault \(vaultID) should not exist after close") log("Vault no longer exists — close confirmed") let abBalFinal = _autoBalancerBalance(vaultID) Test.assert(abBalFinal == nil, - message: "AutoBalancer should be nil (burned) after vault close, but got: ".concat((abBalFinal ?? 0.0).toString())) + message: "AutoBalancer should be nil (burned) after vault close, but got: \(abBalFinal ?? 0.0)") log("AutoBalancer is nil after close — torn down during _cleanupAutoBalancer") let pyusd0After = _pyusd0Balance(pyusd0User) - log("PYUSD0 balance after close: ".concat(pyusd0After.toString())) + log("PYUSD0 balance after close: \(pyusd0After)") // 10 FLOW ≈ $0.3–0.5 at current prices — well above tx fees incurred during this test. // The net gain should be clearly positive: excess syWFLOWv → FLOW → PYUSD0 adds more // PYUSD0 back than the transactions consume in fees. Test.assert( pyusd0After > pyusd0Before, - message: "User should have more PYUSD0 than before (excess syWFLOWv converted back to PYUSD0). " - .concat("Before: ").concat(pyusd0Before.toString()) - .concat(", After: ").concat(pyusd0After.toString()) + message: "User should have more PYUSD0 than before (excess syWFLOWv converted back to PYUSD0). Before: \(pyusd0Before), After: \(pyusd0After)" ) let pyusd0Net = pyusd0After - pyusd0Before - log("Net PYUSD0 gain from excess syWFLOWv conversion: ".concat(pyusd0Net.toString()) - .concat(" PYUSD0 (injected ~").concat(injectionFlowAmount.toString()).concat(" FLOW worth)")) + log("Net PYUSD0 gain from excess syWFLOWv conversion: \(pyusd0Net) PYUSD0 (injected ~\(injectionFlowAmount) FLOW worth)") log("=== testCloseSyWFLOWvVaultWithExcessYieldTokens_PYUSD0 PASSED ===") } From 641de769c07fce19f7da0ec4d21b64b1e4e31766 Mon Sep 17 00:00:00 2001 From: Alex <12097569+nialexsan@users.noreply.github.com> Date: Tue, 31 Mar 2026 21:50:38 -0400 Subject: [PATCH 69/72] add balance assertions and tolerance --- ...wYieldVaultsStrategiesV2_syWFLOWv_test.cdc | 71 ++++++++++++++----- 1 file changed, 53 insertions(+), 18 deletions(-) diff --git a/cadence/tests/FlowYieldVaultsStrategiesV2_syWFLOWv_test.cdc b/cadence/tests/FlowYieldVaultsStrategiesV2_syWFLOWv_test.cdc index cb03af93..460ac64a 100644 --- a/cadence/tests/FlowYieldVaultsStrategiesV2_syWFLOWv_test.cdc +++ b/cadence/tests/FlowYieldVaultsStrategiesV2_syWFLOWv_test.cdc @@ -73,6 +73,9 @@ access(all) var pyusd0VaultID: UInt64 = 0 access(all) var wbtcVaultID: UInt64 = 0 access(all) var wethVaultID: UInt64 = 0 +/// Relative tolerance used in all balance assertions (0.1%). +access(all) let tolerancePct: UFix64 = 0.001 + /* --- Helpers --- */ @@ -133,6 +136,14 @@ fun _pyusd0Balance(_ user: Test.TestAccount): UFix64 { return (r.returnValue as? UFix64) ?? 0.0 } +/// Returns the WBTC Cadence vault balance for the given account. +access(all) +fun _wbtcBalance(_ user: Test.TestAccount): UFix64 { + let r = _executeScript("../scripts/tokens/get_vault_balance_by_type.cdc", [user.address, wbtcVaultIdentifier]) + Test.expect(r, Test.beSucceeded()) + return (r.returnValue as? UFix64) ?? 0.0 +} + /* --- Setup --- */ access(all) fun setup() { @@ -391,10 +402,11 @@ access(all) fun setup() { ========================================================= */ access(all) fun testCreateSyWFLOWvYieldVault_PYUSD0() { - log("Creating syWFLOWvStrategy yield vault with 2.0 PYUSD0...") + let collateralAmount: UFix64 = 2.0 + log("Creating syWFLOWvStrategy yield vault with \(collateralAmount) PYUSD0...") let result = _executeTransactionFile( "../transactions/flow-yield-vaults/create_yield_vault.cdc", - [syWFLOWvStrategyIdentifier, pyusd0VaultIdentifier, 2.0], + [syWFLOWvStrategyIdentifier, pyusd0VaultIdentifier, collateralAmount], [pyusd0User] ) Test.expect(result, Test.beSucceeded()) @@ -406,6 +418,8 @@ access(all) fun testCreateSyWFLOWvYieldVault_PYUSD0() { 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)") + Test.assert(equalAmounts(a: balance!, b: collateralAmount, tolerance: collateralAmount * tolerancePct), + message: "PYUSD0 vault balance after create should be ~\(collateralAmount), got \(balance!)") log("PYUSD0 vault balance after create: \(balance!)") // Verify PYUSD0 was deposited directly into FlowALP as collateral (no intermediate token swap). @@ -444,7 +458,7 @@ access(all) fun testDepositToSyWFLOWvYieldVault_PYUSD0() { 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), + Test.assert(equalAmounts(a: after, b: before + depositAmount, tolerance: (before + depositAmount) * tolerancePct), message: "PYUSD0 deposit: expected ~\(before + depositAmount), got \(after)") log("PYUSD0 vault balance after deposit: \(after)") } @@ -458,14 +472,15 @@ access(all) fun testWithdrawFromSyWFLOWvYieldVault_PYUSD0() { 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), + Test.assert(equalAmounts(a: after, b: before - withdrawAmount, tolerance: (before - withdrawAmount) * tolerancePct), message: "PYUSD0 withdraw: expected ~\(before - withdrawAmount), got \(after)") log("PYUSD0 vault balance after withdrawal: \(after)") } access(all) fun testCloseSyWFLOWvYieldVault_PYUSD0() { let vaultBalBefore = (_executeScript("../scripts/flow-yield-vaults/get_yield_vault_balance.cdc", [pyusd0User.address, pyusd0VaultID]).returnValue! as! UFix64?) ?? 0.0 - log("Closing PYUSD0 vault \(pyusd0VaultID) (balance: \(vaultBalBefore))...") + let userBalBefore = _pyusd0Balance(pyusd0User) + log("Closing PYUSD0 vault \(pyusd0VaultID) (balance: \(vaultBalBefore), user PYUSD0: \(userBalBefore))...") Test.expect( _executeTransactionFile("../transactions/flow-yield-vaults/close_yield_vault.cdc", [pyusd0VaultID], [pyusd0User]), Test.beSucceeded() @@ -473,7 +488,11 @@ access(all) fun testCloseSyWFLOWvYieldVault_PYUSD0() { 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") + let userBalAfter = _pyusd0Balance(pyusd0User) + let returned = userBalAfter - userBalBefore + Test.assert(equalAmounts(a: returned, b: vaultBalBefore, tolerance: vaultBalBefore * tolerancePct), + message: "Expected ~\(vaultBalBefore) PYUSD0 returned on close, got \(returned)") + log("PYUSD0 yield vault closed — returned \(returned) PYUSD0 (vault had \(vaultBalBefore))") } /* ========================================================= @@ -481,10 +500,11 @@ access(all) fun testCloseSyWFLOWvYieldVault_PYUSD0() { ========================================================= */ access(all) fun testCreateSyWFLOWvYieldVault_WBTC() { - log("Creating syWFLOWvStrategy yield vault with 0.0001 WBTC...") + let collateralAmount: UFix64 = 0.0001 + log("Creating syWFLOWvStrategy yield vault with \(collateralAmount) WBTC...") let result = _executeTransactionFile( "../transactions/flow-yield-vaults/create_yield_vault.cdc", - [syWFLOWvStrategyIdentifier, wbtcVaultIdentifier, 0.0001], + [syWFLOWvStrategyIdentifier, wbtcVaultIdentifier, collateralAmount], [wbtcUser] ) Test.expect(result, Test.beSucceeded()) @@ -496,6 +516,8 @@ access(all) fun testCreateSyWFLOWvYieldVault_WBTC() { 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)") + Test.assert(equalAmounts(a: balance!, b: collateralAmount, tolerance: collateralAmount * tolerancePct), + message: "WBTC vault balance after create should be ~\(collateralAmount), got \(balance!)") log("WBTC vault balance after create: \(balance!)") } @@ -508,7 +530,7 @@ access(all) fun testDepositToSyWFLOWvYieldVault_WBTC() { Test.beSucceeded() ) let after = (_executeScript("../scripts/flow-yield-vaults/get_yield_vault_balance.cdc", [wbtcUser.address, wbtcVaultID]).returnValue! as! UFix64?)! - Test.assert(equalAmounts(a: after, b: before + depositAmount, tolerance: 0.000005), + Test.assert(equalAmounts(a: after, b: before + depositAmount, tolerance: (before + depositAmount) * tolerancePct), message: "WBTC deposit: expected ~\(before + depositAmount), got \(after)") log("WBTC vault balance after deposit: \(after)") } @@ -522,14 +544,15 @@ access(all) fun testWithdrawFromSyWFLOWvYieldVault_WBTC() { Test.beSucceeded() ) let after = (_executeScript("../scripts/flow-yield-vaults/get_yield_vault_balance.cdc", [wbtcUser.address, wbtcVaultID]).returnValue! as! UFix64?)! - Test.assert(equalAmounts(a: after, b: before - withdrawAmount, tolerance: 0.000005), + Test.assert(equalAmounts(a: after, b: before - withdrawAmount, tolerance: (before - withdrawAmount) * tolerancePct), message: "WBTC withdraw: expected ~\(before - withdrawAmount), got \(after)") log("WBTC vault balance after withdrawal: \(after)") } access(all) fun testCloseSyWFLOWvYieldVault_WBTC() { let vaultBalBefore = (_executeScript("../scripts/flow-yield-vaults/get_yield_vault_balance.cdc", [wbtcUser.address, wbtcVaultID]).returnValue! as! UFix64?) ?? 0.0 - log("Closing WBTC vault \(wbtcVaultID) (balance: \(vaultBalBefore))...") + let userBalBefore = _wbtcBalance(wbtcUser) + log("Closing WBTC vault \(wbtcVaultID) (balance: \(vaultBalBefore), user WBTC: \(userBalBefore))...") Test.expect( _executeTransactionFile("../transactions/flow-yield-vaults/close_yield_vault.cdc", [wbtcVaultID], [wbtcUser]), Test.beSucceeded() @@ -537,7 +560,11 @@ access(all) fun testCloseSyWFLOWvYieldVault_WBTC() { let vaultBalAfter = _executeScript("../scripts/flow-yield-vaults/get_yield_vault_balance.cdc", [wbtcUser.address, wbtcVaultID]) Test.expect(vaultBalAfter, Test.beSucceeded()) Test.assert(vaultBalAfter.returnValue == nil, message: "WBTC vault should no longer exist after close") - log("WBTC yield vault closed successfully") + let userBalAfter = _wbtcBalance(wbtcUser) + let returned = userBalAfter - userBalBefore + Test.assert(equalAmounts(a: returned, b: vaultBalBefore, tolerance: vaultBalBefore * tolerancePct), + message: "Expected ~\(vaultBalBefore) WBTC returned on close, got \(returned)") + log("WBTC yield vault closed — returned \(returned) WBTC (vault had \(vaultBalBefore))") } /* ========================================================= @@ -545,10 +572,11 @@ access(all) fun testCloseSyWFLOWvYieldVault_WBTC() { ========================================================= */ access(all) fun testCreateSyWFLOWvYieldVault_WETH() { - log("Creating syWFLOWvStrategy yield vault with 0.001 WETH...") + let collateralAmount: UFix64 = 0.001 + log("Creating syWFLOWvStrategy yield vault with \(collateralAmount) WETH...") let result = _executeTransactionFile( "../transactions/flow-yield-vaults/create_yield_vault.cdc", - [syWFLOWvStrategyIdentifier, wethVaultIdentifier, 0.001], + [syWFLOWvStrategyIdentifier, wethVaultIdentifier, collateralAmount], [wethUser] ) Test.expect(result, Test.beSucceeded()) @@ -560,6 +588,8 @@ access(all) fun testCreateSyWFLOWvYieldVault_WETH() { 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)") + Test.assert(equalAmounts(a: balance!, b: collateralAmount, tolerance: collateralAmount * tolerancePct), + message: "WETH vault balance after create should be ~\(collateralAmount), got \(balance!)") log("WETH vault balance after create: \(balance!)") } @@ -572,7 +602,7 @@ access(all) fun testDepositToSyWFLOWvYieldVault_WETH() { Test.beSucceeded() ) let after = (_executeScript("../scripts/flow-yield-vaults/get_yield_vault_balance.cdc", [wethUser.address, wethVaultID]).returnValue! as! UFix64?)! - Test.assert(equalAmounts(a: after, b: before + depositAmount, tolerance: 0.00005), + Test.assert(equalAmounts(a: after, b: before + depositAmount, tolerance: (before + depositAmount) * tolerancePct), message: "WETH deposit: expected ~\(before + depositAmount), got \(after)") log("WETH vault balance after deposit: \(after)") } @@ -586,14 +616,15 @@ access(all) fun testWithdrawFromSyWFLOWvYieldVault_WETH() { Test.beSucceeded() ) let after = (_executeScript("../scripts/flow-yield-vaults/get_yield_vault_balance.cdc", [wethUser.address, wethVaultID]).returnValue! as! UFix64?)! - Test.assert(equalAmounts(a: after, b: before - withdrawAmount, tolerance: 0.00005), + Test.assert(equalAmounts(a: after, b: before - withdrawAmount, tolerance: (before - withdrawAmount) * tolerancePct), message: "WETH withdraw: expected ~\(before - withdrawAmount), got \(after)") log("WETH vault balance after withdrawal: \(after)") } access(all) fun testCloseSyWFLOWvYieldVault_WETH() { let vaultBalBefore = (_executeScript("../scripts/flow-yield-vaults/get_yield_vault_balance.cdc", [wethUser.address, wethVaultID]).returnValue! as! UFix64?) ?? 0.0 - log("Closing WETH vault \(wethVaultID) (balance: \(vaultBalBefore))...") + let userBalBefore = _wethBalance(wethUser) + log("Closing WETH vault \(wethVaultID) (balance: \(vaultBalBefore), user WETH: \(userBalBefore))...") Test.expect( _executeTransactionFile("../transactions/flow-yield-vaults/close_yield_vault.cdc", [wethVaultID], [wethUser]), Test.beSucceeded() @@ -601,7 +632,11 @@ access(all) fun testCloseSyWFLOWvYieldVault_WETH() { let vaultBalAfter = _executeScript("../scripts/flow-yield-vaults/get_yield_vault_balance.cdc", [wethUser.address, wethVaultID]) Test.expect(vaultBalAfter, Test.beSucceeded()) Test.assert(vaultBalAfter.returnValue == nil, message: "WETH vault should no longer exist after close") - log("WETH yield vault closed successfully") + let userBalAfter = _wethBalance(wethUser) + let returned = userBalAfter - userBalBefore + Test.assert(equalAmounts(a: returned, b: vaultBalBefore, tolerance: vaultBalBefore * tolerancePct), + message: "Expected ~\(vaultBalBefore) WETH returned on close, got \(returned)") + log("WETH yield vault closed — returned \(returned) WETH (vault had \(vaultBalBefore))") } /* ========================================================= From 3592735fd9094c221621128417ef2f1dcc1396e7 Mon Sep 17 00:00:00 2001 From: Alex <12097569+nialexsan@users.noreply.github.com> Date: Wed, 1 Apr 2026 08:31:06 -0400 Subject: [PATCH 70/72] remove unnecessary assert --- cadence/tests/FlowYieldVaultsStrategiesV2_syWFLOWv_test.cdc | 3 --- 1 file changed, 3 deletions(-) diff --git a/cadence/tests/FlowYieldVaultsStrategiesV2_syWFLOWv_test.cdc b/cadence/tests/FlowYieldVaultsStrategiesV2_syWFLOWv_test.cdc index 460ac64a..66aa0ef0 100644 --- a/cadence/tests/FlowYieldVaultsStrategiesV2_syWFLOWv_test.cdc +++ b/cadence/tests/FlowYieldVaultsStrategiesV2_syWFLOWv_test.cdc @@ -417,7 +417,6 @@ access(all) fun testCreateSyWFLOWvYieldVault_PYUSD0() { 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)") Test.assert(equalAmounts(a: balance!, b: collateralAmount, tolerance: collateralAmount * tolerancePct), message: "PYUSD0 vault balance after create should be ~\(collateralAmount), got \(balance!)") log("PYUSD0 vault balance after create: \(balance!)") @@ -515,7 +514,6 @@ access(all) fun testCreateSyWFLOWvYieldVault_WBTC() { 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)") Test.assert(equalAmounts(a: balance!, b: collateralAmount, tolerance: collateralAmount * tolerancePct), message: "WBTC vault balance after create should be ~\(collateralAmount), got \(balance!)") log("WBTC vault balance after create: \(balance!)") @@ -587,7 +585,6 @@ access(all) fun testCreateSyWFLOWvYieldVault_WETH() { 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)") Test.assert(equalAmounts(a: balance!, b: collateralAmount, tolerance: collateralAmount * tolerancePct), message: "WETH vault balance after create should be ~\(collateralAmount), got \(balance!)") log("WETH vault balance after create: \(balance!)") From 2bbcad5c6f3782edeeaee33ed434e330bf28838b Mon Sep 17 00:00:00 2001 From: Alex <12097569+nialexsan@users.noreply.github.com> Date: Wed, 1 Apr 2026 09:18:50 -0400 Subject: [PATCH 71/72] tighten assertion --- cadence/tests/FlowYieldVaultsStrategiesV2_syWFLOWv_test.cdc | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cadence/tests/FlowYieldVaultsStrategiesV2_syWFLOWv_test.cdc b/cadence/tests/FlowYieldVaultsStrategiesV2_syWFLOWv_test.cdc index 66aa0ef0..b7c8de9b 100644 --- a/cadence/tests/FlowYieldVaultsStrategiesV2_syWFLOWv_test.cdc +++ b/cadence/tests/FlowYieldVaultsStrategiesV2_syWFLOWv_test.cdc @@ -721,8 +721,8 @@ access(all) fun testCloseSyWFLOWvVaultWithExcessYieldTokens_PYUSD0() { ) Test.expect(vaultBalAfterCreate, Test.beSucceeded()) let vaultBal = vaultBalAfterCreate.returnValue! as! UFix64? - Test.assert(vaultBal != nil && vaultBal! > 0.0, - message: "Expected positive vault balance after create, got: \(vaultBal ?? 0.0)") + Test.assert(equalAmounts(a: vaultBal!, b: collateralAmount, tolerance: collateralAmount * tolerancePct), + message: "Expected vault balance ~\(collateralAmount) after create, got: \(vaultBal ?? 0.0)") log("Vault balance (PYUSD0 collateral value): \(vaultBal!)") let abBalBefore = _autoBalancerBalance(vaultID) From 8d65034136f487060b88c09ff89b1cea3e64de2a Mon Sep 17 00:00:00 2001 From: Alex <12097569+nialexsan@users.noreply.github.com> Date: Wed, 1 Apr 2026 12:44:31 -0400 Subject: [PATCH 72/72] tolerance in FUSDEV tests --- ...lowYieldVaultsStrategiesV2_FUSDEV_test.cdc | 27 ++++++++++--------- ...wYieldVaultsStrategiesV2_syWFLOWv_test.cdc | 2 +- 2 files changed, 16 insertions(+), 13 deletions(-) diff --git a/cadence/tests/FlowYieldVaultsStrategiesV2_FUSDEV_test.cdc b/cadence/tests/FlowYieldVaultsStrategiesV2_FUSDEV_test.cdc index 969feeda..2fce0197 100644 --- a/cadence/tests/FlowYieldVaultsStrategiesV2_FUSDEV_test.cdc +++ b/cadence/tests/FlowYieldVaultsStrategiesV2_FUSDEV_test.cdc @@ -85,6 +85,9 @@ access(all) var flowVaultID: UInt64 = 0 access(all) var wbtcVaultID: UInt64 = 0 access(all) var wethVaultID: UInt64 = 0 +/// Relative tolerance used in all balance assertions (1%). +access(all) let tolerancePct: UFix64 = 0.01 + /* --- Helpers --- */ access(all) @@ -442,7 +445,7 @@ access(all) fun testDepositToFUSDEVYieldVault_WFLOW() { Test.beSucceeded() ) let after = (_executeScript("../scripts/flow-yield-vaults/get_yield_vault_balance.cdc", [flowUser.address, flowVaultID]).returnValue! as! UFix64?)! - Test.assert(equalAmounts(a: after, b: before + depositAmount, tolerance: 0.1), + Test.assert(equalAmounts(a: after, b: before + depositAmount, tolerance: (before + depositAmount) * tolerancePct), message: "WFLOW deposit: expected ~".concat((before + depositAmount).toString()).concat(", got ").concat(after.toString())) log("WFLOW vault balance after deposit: ".concat(after.toString())) } @@ -456,7 +459,7 @@ access(all) fun testWithdrawFromFUSDEVYieldVault_WFLOW() { Test.beSucceeded() ) let after = (_executeScript("../scripts/flow-yield-vaults/get_yield_vault_balance.cdc", [flowUser.address, flowVaultID]).returnValue! as! UFix64?)! - Test.assert(equalAmounts(a: after, b: before - withdrawAmount, tolerance: 0.1), + Test.assert(equalAmounts(a: after, b: before - withdrawAmount, tolerance: (before - withdrawAmount) * tolerancePct), message: "WFLOW withdraw: expected ~".concat((before - withdrawAmount).toString()).concat(", got ").concat(after.toString())) log("WFLOW vault balance after withdrawal: ".concat(after.toString())) } @@ -476,7 +479,7 @@ access(all) fun testCloseFUSDEVYieldVault_WFLOW() { // After close the debt is fully repaid (closePosition would have reverted otherwise). // Assert that the collateral returned is within 5% of the vault NAV before close, // accounting for UniV3 swap fees and any pre-supplement collateral sold to cover shortfall. - Test.assert(equalAmounts(a: collateralAfter, b: collateralBefore + vaultBalBefore, tolerance: vaultBalBefore / 20.0), + Test.assert(equalAmounts(a: collateralAfter, b: collateralBefore + vaultBalBefore, tolerance: vaultBalBefore * tolerancePct), message: "WFLOW close: expected ~".concat(vaultBalBefore.toString()).concat(" FLOW returned, collateralBefore=").concat(collateralBefore.toString()).concat(" collateralAfter=").concat(collateralAfter.toString())) log("WFLOW yield vault closed successfully, collateral returned: ".concat(collateralAfter.toString())) } @@ -513,7 +516,7 @@ access(all) fun testDepositToFUSDEVYieldVault_WBTC() { Test.beSucceeded() ) let after = (_executeScript("../scripts/flow-yield-vaults/get_yield_vault_balance.cdc", [wbtcUser.address, wbtcVaultID]).returnValue! as! UFix64?)! - Test.assert(equalAmounts(a: after, b: before + depositAmount, tolerance: 0.000005), + Test.assert(equalAmounts(a: after, b: before + depositAmount, tolerance: (before + depositAmount) * tolerancePct), message: "WBTC deposit: expected ~".concat((before + depositAmount).toString()).concat(", got ").concat(after.toString())) log("WBTC vault balance after deposit: ".concat(after.toString())) } @@ -527,7 +530,7 @@ access(all) fun testWithdrawFromFUSDEVYieldVault_WBTC() { Test.beSucceeded() ) let after = (_executeScript("../scripts/flow-yield-vaults/get_yield_vault_balance.cdc", [wbtcUser.address, wbtcVaultID]).returnValue! as! UFix64?)! - Test.assert(equalAmounts(a: after, b: before - withdrawAmount, tolerance: 0.000005), + Test.assert(equalAmounts(a: after, b: before - withdrawAmount, tolerance: (before - withdrawAmount) * tolerancePct), message: "WBTC withdraw: expected ~".concat((before - withdrawAmount).toString()).concat(", got ").concat(after.toString())) log("WBTC vault balance after withdrawal: ".concat(after.toString())) } @@ -547,7 +550,7 @@ access(all) fun testCloseFUSDEVYieldVault_WBTC() { let collateralAfter = (_executeScript("../scripts/tokens/get_balance.cdc", [wbtcUser.address, wbtcBalancePath]).returnValue! as! UFix64?) ?? 0.0 // After close the debt is fully repaid (closePosition would have reverted otherwise). // Assert that the collateral returned is within 5% of the vault NAV before close. - Test.assert(equalAmounts(a: collateralAfter, b: collateralBefore + vaultBalBefore, tolerance: vaultBalBefore / 20.0), + Test.assert(equalAmounts(a: collateralAfter, b: collateralBefore + vaultBalBefore, tolerance: vaultBalBefore * tolerancePct), message: "WBTC close: expected ~".concat(vaultBalBefore.toString()).concat(" WBTC returned, collateralBefore=").concat(collateralBefore.toString()).concat(" collateralAfter=").concat(collateralAfter.toString())) log("WBTC yield vault closed successfully, collateral returned: ".concat(collateralAfter.toString())) } @@ -584,7 +587,7 @@ access(all) fun testDepositToFUSDEVYieldVault_WETH() { Test.beSucceeded() ) let after = (_executeScript("../scripts/flow-yield-vaults/get_yield_vault_balance.cdc", [wethUser.address, wethVaultID]).returnValue! as! UFix64?)! - Test.assert(equalAmounts(a: after, b: before + depositAmount, tolerance: 0.00005), + Test.assert(equalAmounts(a: after, b: before + depositAmount, tolerance: (before + depositAmount) * tolerancePct), message: "WETH deposit: expected ~".concat((before + depositAmount).toString()).concat(", got ").concat(after.toString())) log("WETH vault balance after deposit: ".concat(after.toString())) } @@ -598,7 +601,7 @@ access(all) fun testWithdrawFromFUSDEVYieldVault_WETH() { Test.beSucceeded() ) let after = (_executeScript("../scripts/flow-yield-vaults/get_yield_vault_balance.cdc", [wethUser.address, wethVaultID]).returnValue! as! UFix64?)! - Test.assert(equalAmounts(a: after, b: before - withdrawAmount, tolerance: 0.00005), + Test.assert(equalAmounts(a: after, b: before - withdrawAmount, tolerance: (before - withdrawAmount) * tolerancePct), message: "WETH withdraw: expected ~".concat((before - withdrawAmount).toString()).concat(", got ").concat(after.toString())) log("WETH vault balance after withdrawal: ".concat(after.toString())) } @@ -618,7 +621,7 @@ access(all) fun testCloseFUSDEVYieldVault_WETH() { let collateralAfter = (_executeScript("../scripts/tokens/get_balance.cdc", [wethUser.address, wethBalancePath]).returnValue! as! UFix64?) ?? 0.0 // After close the debt is fully repaid (closePosition would have reverted otherwise). // Assert that the collateral returned is within 5% of the vault NAV before close. - Test.assert(equalAmounts(a: collateralAfter, b: collateralBefore + vaultBalBefore, tolerance: vaultBalBefore / 20.0), + Test.assert(equalAmounts(a: collateralAfter, b: collateralBefore + vaultBalBefore, tolerance: vaultBalBefore * tolerancePct), message: "WETH close: expected ~".concat(vaultBalBefore.toString()).concat(" WETH returned, collateralBefore=").concat(collateralBefore.toString()).concat(" collateralAfter=").concat(collateralAfter.toString())) log("WETH yield vault closed successfully, collateral returned: ".concat(collateralAfter.toString())) } @@ -710,12 +713,12 @@ access(all) fun testCloseFUSDEVVaultWithExcessYieldTokens_WFLOW() { ) Test.expect(vaultBalAfterCreate, Test.beSucceeded()) let vaultBal = vaultBalAfterCreate.returnValue! as! UFix64? - Test.assert(vaultBal != nil && vaultBal! > 0.0, - message: "Expected positive vault balance after create, got: \(vaultBal ?? 0.0)") + Test.assert(equalAmounts(a: vaultBal!, b: collateralAmount, tolerance: collateralAmount * tolerancePct), + message: "Expected vault balance ~\(collateralAmount) after create, got: \(vaultBal ?? 0.0)") log("Vault balance (FLOW collateral value): \(vaultBal!)") let abBalBefore = _autoBalancerBalance(vaultID) - Test.assert(abBalBefore != nil && abBalBefore! > 0.0, + Test.assert(abBalBefore! > 0.0, message: "Expected positive AutoBalancer balance after vault creation, got: \(abBalBefore ?? 0.0)") log("AutoBalancer FUSDEV balance before injection: \(abBalBefore!)") diff --git a/cadence/tests/FlowYieldVaultsStrategiesV2_syWFLOWv_test.cdc b/cadence/tests/FlowYieldVaultsStrategiesV2_syWFLOWv_test.cdc index b7c8de9b..19c19ba7 100644 --- a/cadence/tests/FlowYieldVaultsStrategiesV2_syWFLOWv_test.cdc +++ b/cadence/tests/FlowYieldVaultsStrategiesV2_syWFLOWv_test.cdc @@ -726,7 +726,7 @@ access(all) fun testCloseSyWFLOWvVaultWithExcessYieldTokens_PYUSD0() { log("Vault balance (PYUSD0 collateral value): \(vaultBal!)") let abBalBefore = _autoBalancerBalance(vaultID) - Test.assert(abBalBefore != nil && abBalBefore! > 0.0, + Test.assert(abBalBefore! > 0.0, message: "Expected positive AutoBalancer balance after vault creation, got: \(abBalBefore ?? 0.0)") log("AutoBalancer syWFLOWv balance before injection: \(abBalBefore!)")