diff --git a/Dockerfile.mainnet-fork b/Dockerfile.mainnet-fork new file mode 100644 index 00000000..b158695e --- /dev/null +++ b/Dockerfile.mainnet-fork @@ -0,0 +1,26 @@ +FROM debian:stable-slim + +ENV APP_HOME=/app + +RUN apt-get update && apt-get install -y --no-install-recommends \ + curl ca-certificates bash git \ + && rm -rf /var/lib/apt/lists/* + +# Install flow CLI pinned to v2.15.0 (matches e2e workflow) +RUN curl -fsSL "https://raw.githubusercontent.com/onflow/flow-cli/v2.15.0/install.sh" | bash \ + && mv /root/.local/bin/flow /usr/local/bin/flow \ + && flow version + +WORKDIR ${APP_HOME} +COPY . ${APP_HOME} +RUN chmod +x ${APP_HOME}/local/*.sh + +# Pre-install Cadence dependencies at build time (source resolution only; +# mainnet fork state is fetched at container startup, not here) +RUN flow deps install --skip-alias --skip-deployments + +EXPOSE 3569 8888 + +# Runtime: start emulator forked from mainnet. +# The fork state is fetched from access.mainnet.nodes.onflow.org at startup. +ENTRYPOINT ["flow", "emulator", "--fork", "mainnet"] diff --git a/cadence/scripts/diag_pyusd0_owner.cdc b/cadence/scripts/diag_pyusd0_owner.cdc new file mode 100644 index 00000000..5c8c38ba --- /dev/null +++ b/cadence/scripts/diag_pyusd0_owner.cdc @@ -0,0 +1,71 @@ +import "EVM" + +/// Queries owner(), masterMinter(), and supplyController() on the PYUSD0 ERC20 contract +/// by making view EVM calls via a known COA (admin account). +/// +/// Run on fork: +/// flow scripts execute cadence/scripts/diag_pyusd0_owner.cdc --network mainnet-fork +/// Run on mainnet (read-only): +/// flow scripts execute cadence/scripts/diag_pyusd0_owner.cdc --network mainnet + +access(all) fun main(): {String: String} { + let pyusd0 = EVM.addressFromString("0x99aF3EeA856556646C98c8B9b2548Fe815240750") + let zero = EVM.Balance(attoflow: 0) + + // Borrow the admin COA as a plain reference (no entitlements needed for call in view context). + // The admin account (0xb1d63873c3cc9f79) has a COA at /storage/evm. + let adminAcct = getAccount(0xb1d63873c3cc9f79) + let coa = adminAcct.capabilities + .borrow<&EVM.CadenceOwnedAccount>(/public/evm) + + let result: {String: String} = {} + + if coa == nil { + result["error"] = "No public COA capability on admin account — run as a transaction instead" + return result + } + + // owner() — OpenZeppelin Ownable: selector 0x8da5cb5b + let ownerRes = coa!.call( + to: pyusd0, + data: EVM.encodeABIWithSignature("owner()", []), + gasLimit: 50_000, + value: zero + ) + if ownerRes.status == EVM.Status.successful && ownerRes.data.length >= 32 { + let decoded = EVM.decodeABI(types: [Type()], data: ownerRes.data) + result["owner"] = (decoded[0] as! EVM.EVMAddress).toString() + } else { + result["owner"] = "not available (".concat(ownerRes.errorMessage).concat(")") + } + + // masterMinter() — Circle FiatToken pattern: selector 0x35d99f35 + let mmRes = coa!.call( + to: pyusd0, + data: EVM.encodeABIWithSignature("masterMinter()", []), + gasLimit: 50_000, + value: zero + ) + if mmRes.status == EVM.Status.successful && mmRes.data.length >= 32 { + let decoded = EVM.decodeABI(types: [Type()], data: mmRes.data) + result["masterMinter"] = (decoded[0] as! EVM.EVMAddress).toString() + } else { + result["masterMinter"] = "not available (".concat(mmRes.errorMessage).concat(")") + } + + // supplyController() — Paxos pattern: selector 0x9720c7fb + let scRes = coa!.call( + to: pyusd0, + data: EVM.encodeABIWithSignature("supplyController()", []), + gasLimit: 50_000, + value: zero + ) + if scRes.status == EVM.Status.successful && scRes.data.length >= 32 { + let decoded = EVM.decodeABI(types: [Type()], data: scRes.data) + result["supplyController"] = (decoded[0] as! EVM.EVMAddress).toString() + } else { + result["supplyController"] = "not available (".concat(scRes.errorMessage).concat(")") + } + + return result +} diff --git a/cadence/tests/transactions/provision_token_from_flow.cdc b/cadence/tests/transactions/provision_token_from_flow.cdc new file mode 100644 index 00000000..778b0978 --- /dev/null +++ b/cadence/tests/transactions/provision_token_from_flow.cdc @@ -0,0 +1,98 @@ +import "FungibleToken" +import "FungibleTokenMetadataViews" +import "FlowToken" +import "EVM" +import "UniswapV3SwapConnectors" +import "FlowEVMBridgeConfig" + +/// Swaps native FLOW from the signer's FlowToken vault to a target EVM-bridged token via +/// Uniswap V3, and deposits the result into the signer's Cadence storage. +/// +/// This works because WFLOW (the EVM ERC-20 wrapper for FLOW) is registered in +/// FlowEVMBridgeConfig as the EVM representation of FlowToken.Vault. The Swapper +/// bridges FlowToken → WFLOW in EVM, swaps via UniV3, then bridges the output back. +/// +/// Example usages (mainnet): +/// FLOW → WETH: tokenInEvm = WFLOW, tokenOutEvm = WETH, fee = 3000 +/// FLOW → PYUSD0: tokenInEvm = WFLOW, tokenOutEvm = PYUSD0, fee = 100 +/// +/// @param factoryAddr UniV3 factory EVM address (hex, with 0x prefix) +/// @param routerAddr UniV3 router EVM address +/// @param quoterAddr UniV3 quoter EVM address +/// @param tokenInEvm WFLOW EVM address (must be registered in FlowEVMBridgeConfig as FlowToken.Vault) +/// @param tokenOutEvm Target token EVM address (must be onboarded to FlowEVMBridge) +/// @param fee UniV3 pool fee tier (e.g. 3000 = 0.3%, 100 = 0.01%) +/// @param flowAmount Amount of FLOW (UFix64) to swap +/// +transaction( + factoryAddr: String, + routerAddr: String, + quoterAddr: String, + tokenInEvm: String, + tokenOutEvm: String, + fee: UInt32, + flowAmount: UFix64 +) { + prepare(signer: auth(Storage, BorrowValue, IssueStorageCapabilityController, SaveValue, PublishCapability, UnpublishCapability) &Account) { + + // Issue a COA capability so the Swapper can bridge tokens via the signer's COA. + let coaCap = signer.capabilities.storage.issue(/storage/evm) + + let wflowEVM = EVM.addressFromString(tokenInEvm) + let outEVM = EVM.addressFromString(tokenOutEvm) + + // FlowToken.Vault is the Cadence representation of WFLOW in FlowEVMBridgeConfig. + let flowVaultType = FlowEVMBridgeConfig.getTypeAssociated(with: wflowEVM) + ?? panic("WFLOW EVM address is not registered in FlowEVMBridgeConfig: ".concat(tokenInEvm)) + let outVaultType = FlowEVMBridgeConfig.getTypeAssociated(with: outEVM) + ?? panic("Target EVM address is not registered in FlowEVMBridgeConfig: ".concat(tokenOutEvm)) + + let swapper = UniswapV3SwapConnectors.Swapper( + factoryAddress: EVM.addressFromString(factoryAddr), + routerAddress: EVM.addressFromString(routerAddr), + quoterAddress: EVM.addressFromString(quoterAddr), + tokenPath: [wflowEVM, outEVM], + feePath: [fee], + inVault: flowVaultType, + outVault: outVaultType, + coaCapability: coaCap, + uniqueID: nil + ) + + // Withdraw FLOW from the signer's FlowToken vault. + let flowProvider = signer.storage.borrow( + from: /storage/flowTokenVault + ) ?? panic("Signer has no FlowToken vault at /storage/flowTokenVault") + let inVault <- flowProvider.withdraw(amount: flowAmount) + + // Swap: bridges FLOW → WFLOW in EVM, swaps via UniV3, bridges result back to Cadence. + let outVault <- swapper.swap(quote: nil, inVault: <-inVault) + log("Provisioned ".concat(outVault.balance.toString()).concat(" of ").concat(outVaultType.identifier) + .concat(" from ").concat(flowAmount.toString()).concat(" FLOW")) + + // Ensure the output vault exists in signer's Cadence storage. + let outCompType = CompositeType(outVaultType.identifier) + ?? panic("Cannot construct CompositeType for output vault: ".concat(outVaultType.identifier)) + let outContract = getAccount(outCompType.address!).contracts.borrow<&{FungibleToken}>(name: outCompType.contractName!) + ?? panic("Cannot borrow FungibleToken contract for output token") + let outVaultData = outContract.resolveContractView( + resourceType: outCompType, + viewType: Type() + ) as? FungibleTokenMetadataViews.FTVaultData + ?? panic("Cannot resolve FTVaultData for output token") + + if signer.storage.borrow<&{FungibleToken.Vault}>(from: outVaultData.storagePath) == nil { + signer.storage.save(<-outVaultData.createEmptyVault(), to: outVaultData.storagePath) + signer.capabilities.unpublish(outVaultData.receiverPath) + signer.capabilities.unpublish(outVaultData.metadataPath) + let receiverCap = signer.capabilities.storage.issue<&{FungibleToken.Vault}>(outVaultData.storagePath) + let metadataCap = signer.capabilities.storage.issue<&{FungibleToken.Vault}>(outVaultData.storagePath) + signer.capabilities.publish(receiverCap, at: outVaultData.receiverPath) + signer.capabilities.publish(metadataCap, at: outVaultData.metadataPath) + } + + let receiver = signer.storage.borrow<&{FungibleToken.Receiver}>(from: outVaultData.storagePath) + ?? panic("Cannot borrow receiver for output vault") + receiver.deposit(from: <-outVault) + } +} diff --git a/cadence/tests/transactions/setup_ft_vault.cdc b/cadence/tests/transactions/setup_ft_vault.cdc new file mode 100644 index 00000000..c8c9e884 --- /dev/null +++ b/cadence/tests/transactions/setup_ft_vault.cdc @@ -0,0 +1,35 @@ +import "FungibleToken" +import "FungibleTokenMetadataViews" +import "ViewResolver" + +/// Sets up a FungibleToken vault in the signer's storage if not already present, +/// publishing the standard receiver and metadata capabilities. +/// Works with any FT that implements FungibleTokenMetadataViews (including EVMVMBridgedTokens). +/// +/// @param contractAddress Address of the token contract (e.g. 0x1e4aa0b87d10b141) +/// @param contractName Name of the token contract + +transaction(contractAddress: Address, contractName: String) { + prepare(signer: auth(Storage, Capabilities) &Account) { + let viewResolver = getAccount(contractAddress).contracts.borrow<&{ViewResolver}>(name: contractName) + ?? panic("Cannot borrow ViewResolver for ".concat(contractName)) + + let vaultData = viewResolver.resolveContractView( + resourceType: nil, + viewType: Type() + ) as! FungibleTokenMetadataViews.FTVaultData? + ?? panic("Cannot resolve FTVaultData for ".concat(contractName)) + + if signer.storage.borrow<&{FungibleToken.Vault}>(from: vaultData.storagePath) != nil { + return // already set up + } + + 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) + } +} diff --git a/cadence/tests/transactions/transfer_ft_via_vault_data.cdc b/cadence/tests/transactions/transfer_ft_via_vault_data.cdc new file mode 100644 index 00000000..5ca12445 --- /dev/null +++ b/cadence/tests/transactions/transfer_ft_via_vault_data.cdc @@ -0,0 +1,41 @@ +import "FungibleToken" +import "FungibleTokenMetadataViews" +import "ViewResolver" + +/// Generic FungibleToken transfer that resolves storage/receiver paths via FTVaultData. +/// Works with any FT implementing FungibleTokenMetadataViews (including EVMVMBridgedTokens). +/// +/// @param contractAddress Address of the token contract (e.g. 0x1e4aa0b87d10b141) +/// @param contractName Name of the token contract (e.g. EVMVMBridgedToken_99af3eea856556646c98c8b9b2548fe815240750) +/// @param amount Amount to transfer +/// @param to Recipient Cadence address (must already have receiver capability published) + +transaction(contractAddress: Address, contractName: String, amount: UFix64, to: Address) { + + let sentVault: @{FungibleToken.Vault} + let receiverPath: PublicPath + + prepare(signer: auth(BorrowValue) &Account) { + let viewResolver = getAccount(contractAddress).contracts.borrow<&{ViewResolver}>(name: contractName) + ?? panic("Cannot borrow ViewResolver for ".concat(contractName)) + + let vaultData = viewResolver.resolveContractView( + resourceType: nil, + viewType: Type() + ) as! FungibleTokenMetadataViews.FTVaultData? + ?? panic("Cannot resolve FTVaultData for ".concat(contractName)) + + let vaultRef = signer.storage.borrow( + from: vaultData.storagePath + ) ?? panic("Cannot borrow vault from ".concat(vaultData.storagePath.toString())) + + self.sentVault <- vaultRef.withdraw(amount: amount) + self.receiverPath = vaultData.receiverPath + } + + execute { + let receiverRef = getAccount(to).capabilities.borrow<&{FungibleToken.Receiver}>(self.receiverPath) + ?? panic("Cannot borrow receiver at ".concat(self.receiverPath.toString())) + receiverRef.deposit(from: <-self.sentVault) + } +} diff --git a/cadence/transactions/flow-yield-vaults/admin/grant_beta_to_self.cdc b/cadence/transactions/flow-yield-vaults/admin/grant_beta_to_self.cdc new file mode 100644 index 00000000..7a54af8f --- /dev/null +++ b/cadence/transactions/flow-yield-vaults/admin/grant_beta_to_self.cdc @@ -0,0 +1,25 @@ +import "FlowYieldVaultsClosedBeta" + +/// Single-signer variant: admin grants beta access to their own account. +/// Use when the admin is also the test user (avoids multi-sig complexity in shell scripts). +transaction() { + prepare(admin: auth(BorrowValue, Storage) &Account) { + let handle = admin.storage.borrow( + from: FlowYieldVaultsClosedBeta.AdminHandleStoragePath + ) ?? panic("Missing AdminHandle at AdminHandleStoragePath") + + let cap: Capability = + handle.grantBeta(addr: admin.address) + + let p = FlowYieldVaultsClosedBeta.UserBetaCapStoragePath + + if let t = admin.storage.type(at: p) { + if t == Type>() { + let _ = admin.storage.load>(from: p) + } else { + panic("Unexpected type at UserBetaCapStoragePath: ".concat(t.identifier)) + } + } + admin.storage.save(cap, to: p) + } +} diff --git a/cadence/transactions/flow-yield-vaults/admin/update_flowalp_oracle_threshold.cdc b/cadence/transactions/flow-yield-vaults/admin/update_flowalp_oracle_threshold.cdc new file mode 100644 index 00000000..2349230e --- /dev/null +++ b/cadence/transactions/flow-yield-vaults/admin/update_flowalp_oracle_threshold.cdc @@ -0,0 +1,41 @@ +import "FlowALPv0" +import "BandOracleConnectors" +import "DeFiActions" +import "FungibleTokenConnectors" +import "FungibleToken" + +/// Updates the FlowALP pool's price oracle with a larger staleThreshold. +/// +/// Use in fork testing environments where Band oracle data may be up to +/// several hours old. The emulator fork uses mainnet state at a fixed +/// height; as real time advances the oracle data becomes stale. +/// +/// @param staleThreshold: seconds beyond which oracle data is considered stale +/// Use 86400 (24h) for long-running fork test sessions. +/// +/// Must be signed by the FlowALP pool owner (6b00ff876c299c61). +/// In fork mode, signature validation is disabled, so any key can be used. +transaction(staleThreshold: UInt64) { + let pool: auth(FlowALPv0.EGovernance) &FlowALPv0.Pool + let oracle: {DeFiActions.PriceOracle} + + prepare(signer: auth(BorrowValue, IssueStorageCapabilityController) &Account) { + self.pool = signer.storage.borrow(from: FlowALPv0.PoolStoragePath) + ?? panic("Could not borrow reference to Pool from \(FlowALPv0.PoolStoragePath)") + let defaultToken = self.pool.getDefaultToken() + + let vaultCap = signer.capabilities.storage.issue(/storage/flowTokenVault) + let feeSource = FungibleTokenConnectors.VaultSource(min: nil, withdrawVault: vaultCap, uniqueID: nil) + self.oracle = BandOracleConnectors.PriceOracle( + unitOfAccount: defaultToken, + staleThreshold: staleThreshold, + feeSource: feeSource, + uniqueID: nil, + ) + } + + execute { + self.pool.setPriceOracle(self.oracle) + log("FlowALP oracle staleThreshold updated to \(staleThreshold)s") + } +} diff --git a/flow.json b/flow.json index 02e86bc9..c782193c 100644 --- a/flow.json +++ b/flow.json @@ -1034,7 +1034,6 @@ "type": "file", "location": "local/emulator-account.pkey" } - }, "testnet-flow-alp-deployer": { "address": "426f0458ced60037", @@ -1043,6 +1042,20 @@ "hashAlgorithm": "SHA2_256", "resourceID": "projects/dl-flow-devex-staging/locations/us-central1/keyRings/tidal-keyring/cryptoKeys/tidal_admin_pk/cryptoKeyVersions/1" } + }, + "mainnet-fork-flowalp": { + "address": "6b00ff876c299c61", + "key": { + "type": "file", + "location": "local/emulator-account.pkey" + } + }, + "mainnet-fork-pyusd0-donor": { + "address": "443472749ebdaac8", + "key": { + "type": "file", + "location": "local/emulator-account.pkey" + } } }, "deployments": { @@ -1197,16 +1210,6 @@ }, "mainnet-fork": { "mainnet-fork-admin": [ - { - "name": "MockOracle", - "args": [ - { - "value": "A.6b00ff876c299c61.MOET.Vault", - "type": "String" - } - ] - }, - "MockSwapper", "FlowYieldVaultsSchedulerRegistry", "FlowYieldVaultsAutoBalancers", "FlowYieldVaultsSchedulerV1", diff --git a/lib/FlowALP b/lib/FlowALP index 3fc2ab24..67061580 160000 --- a/lib/FlowALP +++ b/lib/FlowALP @@ -1 +1 @@ -Subproject commit 3fc2ab24ce0a8738a5bff1da781316c260663217 +Subproject commit 67061580ee48385a7e45424b74f54807fcd08a43 diff --git a/local/e2e_mainnet_fork.sh b/local/e2e_mainnet_fork.sh new file mode 100755 index 00000000..a503edb4 --- /dev/null +++ b/local/e2e_mainnet_fork.sh @@ -0,0 +1,389 @@ +#!/usr/bin/env bash +# e2e_mainnet_fork.sh — Full lifecycle e2e test for FlowYieldVaultsStrategiesV2 (syWFLOWvStrategy) +# on a Flow emulator forked from mainnet. +# +# Usage: +# cd cadence/FlowYieldVaults +# ./local/e2e_mainnet_fork.sh +# +# Prerequisites: +# - flow CLI installed and in PATH +# - git submodules initialised + flow deps installed (run once): +# git submodule update --init --recursive +# flow deps install +# - local/emulator-account.pkey present (the mainnet-fork-admin key) +# +# What this script tests: +# 1. Admin setup: deploy contracts, recreate issuer, configure syWFLOWvStrategy +# for PYUSD0 and WETH collateral, register strategy. +# 2. Token provisioning: swap native FLOW → WETH and FLOW → PYUSD0 via UniV3 +# using the admin's COA on the forked mainnet EVM. +# 3. Full WETH vault lifecycle: create → deposit → withdraw → close +# 4. Full PYUSD0 vault lifecycle: create → deposit → withdraw → close + +set -euo pipefail + +# --------------------------------------------------------------------------- +# Config +# --------------------------------------------------------------------------- + +NETWORK="mainnet-fork" +SIGNER="mainnet-fork-admin" +ADMIN_CADENCE_ADDR="0xb1d63873c3cc9f79" +ADMIN_COA_EVM_ADDR="0x000000000000000000000002bd91ec0b3c1284fe" + +# EVM addresses (mainnet) +WFLOW_EVM="0xd3bF53DAC106A0290B0483EcBC89d40FcC961f3e" +WETH_EVM="0x2F6F07CDcf3588944Bf4C42aC74ff24bF56e7590" +PYUSD0_EVM="0x99aF3EeA856556646C98c8B9b2548Fe815240750" +MOET_EVM="0x213979bb8a9a86966999b3aa797c1fcf3b967ae2" +SYWFLOWV_EVM="0xCBf9a7753F9D2d0e8141ebB36d99f87AcEf98597" +UNIV3_FACTORY="0xca6d7Bb03334bBf135902e1d919a5feccb461632" +UNIV3_ROUTER="0xeEDC6Ff75e1b10B903D9013c358e446a73d35341" +UNIV3_QUOTER="0x370A8DF17742867a44e56223EC20D82092242C85" + +# Cadence vault type identifiers (mainnet bridge contract 0x1e4aa0b87d10b141) +WETH_VAULT_TYPE="A.1e4aa0b87d10b141.EVMVMBridgedToken_2f6f07cdcf3588944bf4c42ac74ff24bf56e7590.Vault" +PYUSD0_VAULT_TYPE="A.1e4aa0b87d10b141.EVMVMBridgedToken_99af3eea856556646c98c8b9b2548fe815240750.Vault" + +# Strategy identifiers (deployed to admin account on fork) +STRATEGY_ID="A.b1d63873c3cc9f79.FlowYieldVaultsStrategiesV2.syWFLOWvStrategy" +COMPOSER_ID="A.b1d63873c3cc9f79.FlowYieldVaultsStrategiesV2.MoreERC4626StrategyComposer" +ISSUER_PATH="/storage/FlowYieldVaultsStrategyV2ComposerIssuer_0xb1d63873c3cc9f79" + +# FlowALP pool owner — used to refresh the oracle after forking. +# In fork mode, signature validation is disabled, so any key can sign for any address. +FLOWALP_POOL_OWNER="mainnet-fork-flowalp" + +# --------------------------------------------------------------------------- +# Helpers +# --------------------------------------------------------------------------- + +run_txn() { + local desc="$1" + shift + echo "" + echo ">>> $desc" + local result + result=$(flow transactions send "$@" --network "$NETWORK" --signer "$SIGNER" 2>&1 || true) + echo "$result" + if ! echo "$result" | grep -q "SEALED"; then + echo "❌ FAIL: '$desc' (not SEALED)" + exit 1 + fi + if echo "$result" | grep -q "Transaction Error"; then + echo "❌ FAIL: '$desc' (Transaction Error)" + exit 1 + fi + echo "✓ $desc" +} + +run_script() { + local desc="$1" + shift + echo "" + echo ">>> [script] $desc" + flow scripts execute "$@" --network "$NETWORK" 2>&1 +} + +# Get the latest (highest) vault ID for the admin account +get_latest_vault_id() { + local result + result=$(flow scripts execute \ + ./cadence/scripts/flow-yield-vaults/get_yield_vault_ids.cdc \ + "$ADMIN_CADENCE_ADDR" \ + --network "$NETWORK" 2>&1) + echo "$result" | grep -oE '\b[0-9]+\b' | sort -n | tail -1 +} + +# --------------------------------------------------------------------------- +# Step 0: Start emulator forked from mainnet +# --------------------------------------------------------------------------- + +echo "========================================================" +echo " FlowYieldVaults syWFLOWvStrategy — Mainnet Fork E2E" +echo "========================================================" +echo "" + +# Kill any existing emulator and clear its ports +echo ">>> Stopping any existing Flow emulator..." +pkill -9 -f "flow emulator" 2>/dev/null || true +# Also kill any flow process holding our ports (in case pkill missed it) +for port in 3569 8888 8080 2345 2346; do + fuser -k ${port}/tcp 2>/dev/null || true +done +sleep 2 + +echo ">>> Starting Flow emulator forked from mainnet..." +flow emulator --fork mainnet --debugger-port 2346 & +EMULATOR_PID=$! + +# Wait for emulator to be ready (REST API port 8888) +echo ">>> Waiting for emulator REST API at :8888..." +for i in $(seq 1 60); do + if (echo > /dev/tcp/127.0.0.1/8888) 2>/dev/null; then + echo " Emulator ready after ${i}s" + break + fi + if [ "$i" -eq 60 ]; then + echo "❌ Emulator did not start within 60s" + kill "$EMULATOR_PID" 2>/dev/null || true + exit 1 + fi + sleep 1 +done + +# Ensure emulator is killed on exit +trap 'echo ""; echo "Stopping emulator..."; kill "$EMULATOR_PID" 2>/dev/null || true' EXIT + +# --------------------------------------------------------------------------- +# Step 1: Deploy local contracts over the forked state +# --------------------------------------------------------------------------- + +echo "" +echo "=== Step 1: Deploy contracts ===" +echo ">>> flow project deploy --network mainnet-fork --update" +flow project deploy --network "$NETWORK" --update + +# Extend the FlowALP price oracle stale threshold to 24 hours. +# The mainnet Band oracle data at the fork height goes stale after 1 hour as real +# time advances in the emulator. Setting staleThreshold=86400 keeps it valid for +# the entire test session. Signed as the pool owner; sig validation disabled in fork. +echo "" +echo ">>> Extending FlowALP oracle staleThreshold to 24h (fork sig bypass)" +result=$(flow transactions send \ + ./cadence/transactions/flow-yield-vaults/admin/update_flowalp_oracle_threshold.cdc \ + 86400 \ + --network "$NETWORK" --signer "$FLOWALP_POOL_OWNER" --compute-limit 9999 2>&1 || true) +if echo "$result" | grep -q "SEALED" && ! echo "$result" | grep -q "Transaction Error"; then + echo "✓ FlowALP oracle staleThreshold set to 86400s" +else + echo "❌ FlowALP oracle update failed:" + echo "$result" | grep -E "Error|error" | head -3 + exit 1 +fi + +# --------------------------------------------------------------------------- +# Step 2: Admin setup — configure strategy, register +# --------------------------------------------------------------------------- + +echo "" +echo "=== Step 2: Admin setup ===" + +# syWFLOWvStrategy + WETH collateral +# yieldToUnderlying: syWFLOWv → WFLOW, fee 100 (0.01%) +# debtToCollateral: WFLOW → WETH, fee 3000 (0.3%) +run_txn "Configure syWFLOWvStrategy + WETH collateral" \ + ./cadence/transactions/flow-yield-vaults/admin/upsert_more_erc4626_config.cdc \ + "$STRATEGY_ID" \ + "$WETH_VAULT_TYPE" \ + "$SYWFLOWV_EVM" \ + '["0xCBf9a7753F9D2d0e8141ebB36d99f87AcEf98597","0xd3bF53DAC106A0290B0483EcBC89d40FcC961f3e"]' \ + '[100]' \ + '["0xd3bF53DAC106A0290B0483EcBC89d40FcC961f3e","0x2F6F07CDcf3588944Bf4C42aC74ff24bF56e7590"]' \ + '[3000]' \ + --compute-limit 9999 + +# syWFLOWvStrategy + PYUSD0 collateral +# yieldToUnderlying: syWFLOWv → WFLOW, fee 100 +# debtToCollateral: WFLOW → PYUSD0, fee 3000 (WFLOW/PYUSD0 fee100 pool also available; use 3000 to match test) +run_txn "Configure syWFLOWvStrategy + PYUSD0 collateral" \ + ./cadence/transactions/flow-yield-vaults/admin/upsert_more_erc4626_config.cdc \ + "$STRATEGY_ID" \ + "$PYUSD0_VAULT_TYPE" \ + "$SYWFLOWV_EVM" \ + '["0xCBf9a7753F9D2d0e8141ebB36d99f87AcEf98597","0xd3bF53DAC106A0290B0483EcBC89d40FcC961f3e"]' \ + '[100]' \ + '["0xd3bF53DAC106A0290B0483EcBC89d40FcC961f3e","0x99aF3EeA856556646C98c8B9b2548Fe815240750"]' \ + '[3000]' \ + --compute-limit 9999 + +# PYUSD0 → MOET pre-swap config (FlowALP only accepts MOET as stablecoin collateral) +# PYUSD0/MOET fee 100 pool exists on Flow EVM mainnet +run_txn "Configure MOET pre-swap for PYUSD0 (PYUSD0 → MOET fee 100)" \ + ./cadence/transactions/flow-yield-vaults/admin/upsert_moet_preswap_config.cdc \ + "$COMPOSER_ID" \ + "$PYUSD0_VAULT_TYPE" \ + '["0x99aF3EeA856556646C98c8B9b2548Fe815240750","0x213979bb8a9a86966999b3aa797c1fcf3b967ae2"]' \ + '[100]' \ + --compute-limit 9999 + +run_txn "Register syWFLOWvStrategy in FlowYieldVaults factory" \ + ./cadence/transactions/flow-yield-vaults/admin/add_strategy_composer.cdc \ + "$STRATEGY_ID" \ + "$COMPOSER_ID" \ + "$ISSUER_PATH" \ + --compute-limit 9999 + +# --------------------------------------------------------------------------- +# Step 3: Grant beta access to admin (self-grant) +# --------------------------------------------------------------------------- + +echo "" +echo "=== Step 3: Grant beta access ===" + +run_txn "Grant beta access to admin (self)" \ + ./cadence/transactions/flow-yield-vaults/admin/grant_beta_to_self.cdc + +# --------------------------------------------------------------------------- +# Step 4: Fund admin COA with FLOW for bridge fees +# --------------------------------------------------------------------------- + +echo "" +echo "=== Step 4: Fund admin COA with FLOW ===" + +# The UniswapV3SwapConnectors.Swapper bridges via the COA and needs native FLOW +# in the COA to pay bridge fees. Send 50 FLOW to the admin's COA EVM address. +# (Admin has ~92 FLOW on mainnet fork; keep 20+ FLOW for swap inputs.) +run_txn "Send 50 FLOW to admin COA (EVM bridge fees)" \ + ./lib/flow-evm-bridge/cadence/transactions/flow-token/transfer_flow_to_cadence_or_evm.cdc \ + "$ADMIN_COA_EVM_ADDR" \ + 50.0 \ + --compute-limit 9999 + +# --------------------------------------------------------------------------- +# Step 5: Provision collateral tokens by swapping FLOW via UniV3 +# --------------------------------------------------------------------------- + +echo "" +echo "=== Step 5: Provision collateral tokens ===" + +# FLOW → WETH via WFLOW/WETH fee-3000 pool. +# At mainnet fork price (~$0.50/FLOW, ~$2500/WETH), 15 FLOW ≈ 0.003 WETH. +# We only need a small amount for the test (create 0.0001 WETH). +run_txn "Swap 15.0 FLOW → WETH (WFLOW/WETH fee 3000)" \ + ./cadence/tests/transactions/provision_token_from_flow.cdc \ + "$UNIV3_FACTORY" \ + "$UNIV3_ROUTER" \ + "$UNIV3_QUOTER" \ + "$WFLOW_EVM" \ + "$WETH_EVM" \ + 3000 \ + 15.0 \ + --compute-limit 9999 + +# Initialise admin's PYUSD0 Cadence vault (creates empty vault + receiver cap if absent). +run_txn "Setup admin PYUSD0 vault" \ + ./cadence/tests/transactions/setup_ft_vault.cdc \ + "0x1e4aa0b87d10b141" \ + "EVMVMBridgedToken_99af3eea856556646c98c8b9b2548fe815240750" \ + --compute-limit 9999 + +# Transfer PYUSD0 from donor account (0x443472749ebdaac8) to admin. +# Fork mode disables signature validation so we can sign as any address. +# NOTE: cannot use run_txn here — that helper always appends --signer $SIGNER (admin), +# which would override the donor signer. Use a direct call instead. +echo "" +echo ">>> Transfer 2.0 PYUSD0 from donor (0x443472749ebdaac8) to admin" +_donor_result=$(flow transactions send \ + ./cadence/tests/transactions/transfer_ft_via_vault_data.cdc \ + "0x1e4aa0b87d10b141" \ + "EVMVMBridgedToken_99af3eea856556646c98c8b9b2548fe815240750" \ + 2.0 \ + "$ADMIN_CADENCE_ADDR" \ + --network "$NETWORK" --signer "mainnet-fork-pyusd0-donor" --compute-limit 9999 2>&1 || true) +echo "$_donor_result" +if ! echo "$_donor_result" | grep -q "SEALED"; then + echo "❌ FAIL: 'Transfer PYUSD0 from donor' (not SEALED)" + exit 1 +fi +if echo "$_donor_result" | grep -q "Transaction Error"; then + echo "❌ FAIL: 'Transfer PYUSD0 from donor' (Transaction Error)" + exit 1 +fi +echo "✓ Transfer 2.0 PYUSD0 from donor to admin" + +echo "" +run_script "Admin WETH balance" \ + ./cadence/scripts/tokens/get_balance.cdc \ + "$ADMIN_CADENCE_ADDR" \ + "/public/EVMVMBridgedToken_2f6f07cdcf3588944bf4c42ac74ff24bf56e7590Receiver" 2>/dev/null || true + +run_script "Admin PYUSD0 balance" \ + ./cadence/scripts/tokens/get_balance.cdc \ + "$ADMIN_CADENCE_ADDR" \ + "/public/EVMVMBridgedToken_99af3eea856556646c98c8b9b2548fe815240750Receiver" 2>/dev/null || true + +# --------------------------------------------------------------------------- +# Step 6: WETH vault lifecycle +# --------------------------------------------------------------------------- + +echo "" +echo "=== Step 6: WETH vault lifecycle ===" + +# ~0.00021 WETH available from provision; create with 0.0001, deposit 0.00005, withdraw 0.00003 +run_txn "Create WETH yield vault (0.0001 WETH)" \ + ./cadence/transactions/flow-yield-vaults/create_yield_vault.cdc \ + "$STRATEGY_ID" \ + "$WETH_VAULT_TYPE" \ + 0.0001 \ + --compute-limit 9999 + +WETH_VAULT_ID=$(get_latest_vault_id) +echo " WETH vault ID: $WETH_VAULT_ID" + +run_txn "Deposit 0.00005 WETH to vault $WETH_VAULT_ID" \ + ./cadence/transactions/flow-yield-vaults/deposit_to_yield_vault.cdc \ + "$WETH_VAULT_ID" \ + 0.00005 \ + --compute-limit 9999 + +run_txn "Withdraw 0.00003 WETH from vault $WETH_VAULT_ID" \ + ./cadence/transactions/flow-yield-vaults/withdraw_from_yield_vault.cdc \ + "$WETH_VAULT_ID" \ + 0.00003 \ + --compute-limit 9999 + +run_txn "Close WETH vault $WETH_VAULT_ID" \ + ./cadence/transactions/flow-yield-vaults/close_yield_vault.cdc \ + "$WETH_VAULT_ID" \ + --compute-limit 9999 + +echo "✅ WETH lifecycle complete" +sleep 3 + +# --------------------------------------------------------------------------- +# Step 7: PYUSD0 vault lifecycle +# --------------------------------------------------------------------------- + +echo "" +echo "=== Step 7: PYUSD0 vault lifecycle ===" + +# ~0.616 PYUSD0 available from provision; create with 0.3, deposit 0.1, withdraw 0.05 +run_txn "Create PYUSD0 yield vault (0.3 PYUSD0)" \ + ./cadence/transactions/flow-yield-vaults/create_yield_vault.cdc \ + "$STRATEGY_ID" \ + "$PYUSD0_VAULT_TYPE" \ + 0.3 \ + --compute-limit 9999 + +PYUSD0_VAULT_ID=$(get_latest_vault_id) +echo " PYUSD0 vault ID: $PYUSD0_VAULT_ID" + +run_txn "Deposit 0.1 PYUSD0 to vault $PYUSD0_VAULT_ID" \ + ./cadence/transactions/flow-yield-vaults/deposit_to_yield_vault.cdc \ + "$PYUSD0_VAULT_ID" \ + 0.1 \ + --compute-limit 9999 + +run_txn "Withdraw 0.05 PYUSD0 from vault $PYUSD0_VAULT_ID" \ + ./cadence/transactions/flow-yield-vaults/withdraw_from_yield_vault.cdc \ + "$PYUSD0_VAULT_ID" \ + 0.05 \ + --compute-limit 9999 + +run_txn "Close PYUSD0 vault $PYUSD0_VAULT_ID" \ + ./cadence/transactions/flow-yield-vaults/close_yield_vault.cdc \ + "$PYUSD0_VAULT_ID" \ + --compute-limit 9999 + +echo "✅ PYUSD0 lifecycle complete" + +# --------------------------------------------------------------------------- +# Done +# --------------------------------------------------------------------------- + +echo "" +echo "========================================================" +echo " ✅ All E2E transactions SEALED successfully!" +echo "========================================================" diff --git a/local/setup_mainnet_fork_docker.sh b/local/setup_mainnet_fork_docker.sh new file mode 100755 index 00000000..973f2897 --- /dev/null +++ b/local/setup_mainnet_fork_docker.sh @@ -0,0 +1,158 @@ +#!/usr/bin/env bash +# setup_mainnet_fork_docker.sh — One-shot setup for the mainnet-fork docker service. +# +# Runs after the flow-emulator-fork service is healthy. Deploys contracts and +# configures the backend-relevant state (strategies, oracle, beta access, COA funding). +# The vault lifecycle tests from e2e_mainnet_fork.sh are intentionally omitted here. +# +# Usage (via docker-compose): +# Invoked automatically by the flow-fork-setup service. +# +# Environment: +# FLOW_HOST — gRPC host of the running emulator (default: flow-emulator-fork:3569) + +set -euo pipefail + +FLOW_HOST="${FLOW_HOST:-flow-emulator-fork:3569}" +NETWORK="mainnet-fork" +SIGNER="mainnet-fork-admin" +FLOWALP_POOL_OWNER="mainnet-fork-flowalp" + +ADMIN_CADENCE_ADDR="0xb1d63873c3cc9f79" +ADMIN_COA_EVM_ADDR="0x000000000000000000000002bd91ec0b3c1284fe" + +WFLOW_EVM="0xd3bF53DAC106A0290B0483EcBC89d40FcC961f3e" +WETH_EVM="0x2F6F07CDcf3588944Bf4C42aC74ff24bF56e7590" +PYUSD0_EVM="0x99aF3EeA856556646C98c8B9b2548Fe815240750" +MOET_EVM="0x213979bb8a9a86966999b3aa797c1fcf3b967ae2" +SYWFLOWV_EVM="0xCBf9a7753F9D2d0e8141ebB36d99f87AcEf98597" + +WETH_VAULT_TYPE="A.1e4aa0b87d10b141.EVMVMBridgedToken_2f6f07cdcf3588944bf4c42ac74ff24bf56e7590.Vault" +PYUSD0_VAULT_TYPE="A.1e4aa0b87d10b141.EVMVMBridgedToken_99af3eea856556646c98c8b9b2548fe815240750.Vault" + +STRATEGY_ID="A.b1d63873c3cc9f79.FlowYieldVaultsStrategiesV2.syWFLOWvStrategy" +COMPOSER_ID="A.b1d63873c3cc9f79.FlowYieldVaultsStrategiesV2.MoreERC4626StrategyComposer" +ISSUER_PATH="/storage/FlowYieldVaultsStrategyV2ComposerIssuer_0xb1d63873c3cc9f79" + +# --------------------------------------------------------------------------- +# Helpers +# --------------------------------------------------------------------------- + +run_txn() { + local desc="$1" + shift + echo "" + echo ">>> $desc" + local result + result=$(flow transactions send "$@" \ + --network "$NETWORK" --host "$FLOW_HOST" --signer "$SIGNER" 2>&1 || true) + echo "$result" + if ! echo "$result" | grep -q "SEALED"; then + echo "❌ FAIL: '$desc' (not SEALED)" + exit 1 + fi + if echo "$result" | grep -q "Transaction Error"; then + echo "❌ FAIL: '$desc' (Transaction Error)" + exit 1 + fi + echo "✓ $desc" +} + +# --------------------------------------------------------------------------- +# Step 1: Deploy contracts +# --------------------------------------------------------------------------- + +echo "=== Step 1: Deploy contracts ===" +flow project deploy --network "$NETWORK" --host "$FLOW_HOST" --update + +# --------------------------------------------------------------------------- +# Step 2: Extend FlowALP oracle stale threshold to 24h +# --------------------------------------------------------------------------- + +echo "" +echo ">>> Extending FlowALP oracle staleThreshold to 24h (fork sig bypass)" +result=$(flow transactions send \ + ./cadence/transactions/flow-yield-vaults/admin/update_flowalp_oracle_threshold.cdc \ + 86400 \ + --network "$NETWORK" --host "$FLOW_HOST" \ + --signer "$FLOWALP_POOL_OWNER" --compute-limit 9999 2>&1 || true) +echo "$result" +if ! echo "$result" | grep -q "SEALED" || echo "$result" | grep -q "Transaction Error"; then + echo "❌ FlowALP oracle staleThreshold update failed" + exit 1 +fi +echo "✓ Oracle staleThreshold set to 86400s" + +# --------------------------------------------------------------------------- +# Step 3: Configure syWFLOWvStrategy +# --------------------------------------------------------------------------- + +echo "" +echo "=== Step 3: Configure strategies ===" + +run_txn "Configure syWFLOWvStrategy + WETH collateral" \ + ./cadence/transactions/flow-yield-vaults/admin/upsert_more_erc4626_config.cdc \ + "$STRATEGY_ID" \ + "$WETH_VAULT_TYPE" \ + "$SYWFLOWV_EVM" \ + '["0xCBf9a7753F9D2d0e8141ebB36d99f87AcEf98597","0xd3bF53DAC106A0290B0483EcBC89d40FcC961f3e"]' \ + '[100]' \ + '["0xd3bF53DAC106A0290B0483EcBC89d40FcC961f3e","0x2F6F07CDcf3588944Bf4C42aC74ff24bF56e7590"]' \ + '[3000]' \ + --compute-limit 9999 + +run_txn "Configure syWFLOWvStrategy + PYUSD0 collateral" \ + ./cadence/transactions/flow-yield-vaults/admin/upsert_more_erc4626_config.cdc \ + "$STRATEGY_ID" \ + "$PYUSD0_VAULT_TYPE" \ + "$SYWFLOWV_EVM" \ + '["0xCBf9a7753F9D2d0e8141ebB36d99f87AcEf98597","0xd3bF53DAC106A0290B0483EcBC89d40FcC961f3e"]' \ + '[100]' \ + '["0xd3bF53DAC106A0290B0483EcBC89d40FcC961f3e","0x99aF3EeA856556646C98c8B9b2548Fe815240750"]' \ + '[3000]' \ + --compute-limit 9999 + +run_txn "Configure MOET pre-swap for PYUSD0 (PYUSD0 → MOET fee 100)" \ + ./cadence/transactions/flow-yield-vaults/admin/upsert_moet_preswap_config.cdc \ + "$COMPOSER_ID" \ + "$PYUSD0_VAULT_TYPE" \ + '["0x99aF3EeA856556646C98c8B9b2548Fe815240750","0x213979bb8a9a86966999b3aa797c1fcf3b967ae2"]' \ + '[100]' \ + --compute-limit 9999 + +run_txn "Register syWFLOWvStrategy in FlowYieldVaults factory" \ + ./cadence/transactions/flow-yield-vaults/admin/add_strategy_composer.cdc \ + "$STRATEGY_ID" \ + "$COMPOSER_ID" \ + "$ISSUER_PATH" \ + --compute-limit 9999 + +# --------------------------------------------------------------------------- +# Step 4: Grant beta access +# --------------------------------------------------------------------------- + +echo "" +echo "=== Step 4: Grant beta access ===" + +run_txn "Grant beta access to admin (self)" \ + ./cadence/transactions/flow-yield-vaults/admin/grant_beta_to_self.cdc + +# --------------------------------------------------------------------------- +# Step 5: Fund admin COA with FLOW for bridge fees +# --------------------------------------------------------------------------- + +echo "" +echo "=== Step 5: Fund admin COA ===" + +run_txn "Send 50 FLOW to admin COA (EVM bridge fees)" \ + ./lib/flow-evm-bridge/cadence/transactions/flow-token/transfer_flow_to_cadence_or_evm.cdc \ + "$ADMIN_COA_EVM_ADDR" \ + 50.0 \ + --compute-limit 9999 + +# --------------------------------------------------------------------------- +# Done +# --------------------------------------------------------------------------- + +echo "" +echo "✅ Mainnet-fork setup complete. Backend is ready."