diff --git a/.github/workflows/build-flow-emulator.yml b/.github/workflows/build-flow-emulator.yml index ee2553f7..f23505ad 100644 --- a/.github/workflows/build-flow-emulator.yml +++ b/.github/workflows/build-flow-emulator.yml @@ -1,12 +1,9 @@ -name: 'Build and Push Flow Emulator Image' +name: 'Build and Push Mainnet Fork Emulator Image' on: push: branches: - - main - pull_request: - branches: - - main + - v0 # Manual trigger, with a flag to decide if we also push workflow_dispatch: @@ -17,57 +14,51 @@ on: default: false env: - DOCKER_IMAGE_URL: us-west1-docker.pkg.dev/dl-flow-devex-staging/backend/flow-emulator:${{ github.sha }} + IMAGE_NAME: ghcr.io/${{ github.repository_owner }}/flow-emulator-fork jobs: - build: - name: Build Flow Emulator Image + test-and-build: + name: E2E Tests and Build runs-on: ubuntu-latest permissions: contents: read - id-token: write - - outputs: - image-url: ${{ env.DOCKER_IMAGE_URL }} + packages: write steps: - - name: 'Checkout' - uses: 'actions/checkout@v4' + - name: Checkout + uses: actions/checkout@v4 with: submodules: recursive token: ${{ secrets.GH_PAT }} - - name: 'Build Flow Emulator image' + - name: Install Flow CLI run: | - docker build -t "${{ env.DOCKER_IMAGE_URL }}" . - - push: - name: Push Flow Emulator Image - runs-on: ubuntu-latest - needs: build + curl -fsSL https://raw.githubusercontent.com/onflow/flow-cli/v2.16.0/install.sh | bash + echo "${HOME}/.local/bin" >> $GITHUB_PATH - # Only run when manually dispatched *and* push=true - if: ${{ github.event_name == 'workflow_dispatch' && inputs.push == true }} + - name: Install flow dependencies + run: flow deps install - permissions: - contents: read - id-token: write + - name: Run e2e mainnet fork tests + run: ./local/e2e_mainnet_fork.sh - steps: - - id: 'auth' - name: 'Authenticate to Google Cloud' - uses: 'google-github-actions/auth@v2' + - name: Log in to GHCR + if: github.event_name == 'push' || (github.event_name == 'workflow_dispatch' && inputs.push == true) + uses: docker/login-action@v3 with: - workload_identity_provider: '${{ vars.BUILDER_WORKLOAD_IDENTITY_PROVIDER }}' - service_account: '${{ vars.BUILDER_SERVICE_ACCOUNT }}' + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} - - name: 'Set up gcloud' - uses: 'google-github-actions/setup-gcloud@v1' + - name: Build and push flow-emulator-fork + uses: docker/build-push-action@v5 with: - project_id: ${{ vars.GAR_PROJECT_ID }} - - - name: 'Push Flow Emulator image' - run: | - gcloud auth configure-docker ${{ vars.GAR_REGION }}-docker.pkg.dev - docker push "${{ needs.build.outputs.image-url }}" + context: . + file: Dockerfile.mainnet-fork + push: ${{ github.event_name == 'push' || (github.event_name == 'workflow_dispatch' && inputs.push == true) }} + tags: | + ${{ env.IMAGE_NAME }}:latest + ${{ env.IMAGE_NAME }}:${{ github.sha }} + cache-from: type=registry,ref=${{ env.IMAGE_NAME }}:latest + cache-to: type=inline diff --git a/Dockerfile.mainnet-fork b/Dockerfile.mainnet-fork new file mode 100644 index 00000000..25c5c43c --- /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 jq \ + && rm -rf /var/lib/apt/lists/* + +# Install flow CLI pinned to v2.16.0 (matches e2e workflow) +RUN curl -fsSL "https://raw.githubusercontent.com/onflow/flow-cli/v2.16.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", "--persist=false"] diff --git a/cadence/tests/transactions/grant_beta_to_self.cdc b/cadence/tests/transactions/grant_beta_to_self.cdc new file mode 100644 index 00000000..7a54af8f --- /dev/null +++ b/cadence/tests/transactions/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/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/tests/transactions/update_flowalp_oracle_threshold.cdc b/cadence/tests/transactions/update_flowalp_oracle_threshold.cdc new file mode 100644 index 00000000..2349230e --- /dev/null +++ b/cadence/tests/transactions/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 1b4b4fe5..6bf6d0b9 100644 --- a/flow.json +++ b/flow.json @@ -1077,7 +1077,6 @@ "type": "file", "location": "local/emulator-account.pkey" } - }, "testnet-flow-alp-deployer": { "address": "426f0458ced60037", @@ -1086,6 +1085,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": "24263c125b7770e0", + "key": { + "type": "file", + "location": "local/emulator-account.pkey" + } } }, "deployments": { @@ -1248,16 +1261,6 @@ }, "mainnet-fork": { "mainnet-fork-admin": [ - { - "name": "MockOracle", - "args": [ - { - "value": "A.6b00ff876c299c61.MOET.Vault", - "type": "String" - } - ] - }, - "MockSwapper", "UInt64LinkedList", "AutoBalancers", "FlowYieldVaultsSchedulerRegistry", diff --git a/lib/FlowALP b/lib/FlowALP index aa10eff7..dc344ca6 160000 --- a/lib/FlowALP +++ b/lib/FlowALP @@ -1 +1 @@ -Subproject commit aa10eff7bfd7a0e351b37ff9ef79a106003d66be +Subproject commit dc344ca6e724c16fde2442fac909304d5cda2e62 diff --git a/local/e2e_mainnet_fork.sh b/local/e2e_mainnet_fork.sh new file mode 100755 index 00000000..7f43dca7 --- /dev/null +++ b/local/e2e_mainnet_fork.sh @@ -0,0 +1,250 @@ +#!/usr/bin/env bash +# e2e_mainnet_fork.sh — Full lifecycle e2e test for FlowYieldVaultsStrategiesV2 on a Flow +# emulator forked from mainnet. +# +# Usage: +# cd cadence/FlowYieldVaults +# ./local/e2e_mainnet_fork.sh +# +# Prerequisites: +# - flow CLI and jq 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, configure syWFLOWvStrategy (PYUSD0 collateral) +# and FUSDEVStrategy (FLOW collateral), register strategies. +# 2. Token provisioning: transfer PYUSD0 from donor account (0x24263c125b7770e0). +# 3. Full PYUSD0 vault lifecycle: create → deposit → withdraw → close (syWFLOWvStrategy) +# 4. Seed FlowALP pool with PYUSD0 for FUSDEVStrategy drawdowns. +# 5. Full FLOW vault lifecycle: create → deposit → withdraw → close (FUSDEVStrategy) + +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +# shellcheck source=./mainnet_fork_common.sh +source "$SCRIPT_DIR/mainnet_fork_common.sh" + +PYUSD0_DONOR="mainnet-fork-pyusd0-donor" + +# --------------------------------------------------------------------------- +# Helpers +# --------------------------------------------------------------------------- + +add_fork_account() { + local name="$1" + local address="$2" + jq --arg name "$name" --arg addr "$address" \ + '.accounts[$name] = {address: $addr, key: {type: "file", location: "local/emulator-account.pkey"}}' \ + flow.json > flow.json.tmp && mv flow.json.tmp flow.json + echo "✓ Registered fork account: $name ($address)" +} + +remove_fork_accounts() { + jq "del(.accounts[\"$PYUSD0_DONOR\"])" \ + flow.json > flow.json.tmp && mv flow.json.tmp flow.json + echo "✓ Removed donor accounts from flow.json" +} + +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 +} + +transfer_token() { + local desc="$1" + local contract_addr="$2" + local contract_name="$3" + local amount="$4" + local recipient="$5" + local donor_signer="$6" + echo "" + echo ">>> $desc" + local result + result=$(flow transactions send \ + ./cadence/tests/transactions/transfer_ft_via_vault_data.cdc \ + "$contract_addr" "$contract_name" "$amount" "$recipient" \ + --network "$NETWORK" --signer "$donor_signer" --compute-limit 9999 2>&1 || true) + echo "$result" + _check_sealed "$desc" "$result" +} + +# --------------------------------------------------------------------------- +# Step 0: Setup +# --------------------------------------------------------------------------- + +echo "========================================================" +echo " FlowYieldVaults — Mainnet Fork E2E" +echo "========================================================" +echo "" + +# Register donor account into flow.json at runtime. +add_fork_account "$PYUSD0_DONOR" "24263c125b7770e0" + +# Kill any existing emulator and clear its ports +echo ">>> Stopping any existing Flow emulator..." +pkill -9 -f "flow emulator" 2>/dev/null || true +sleep 3 +# Wait until all emulator ports are free +for port in 3569 8888 2345; do + for i in $(seq 1 15); do + if ! (echo > /dev/tcp/127.0.0.1/$port) 2>/dev/null; then break; fi + echo " Waiting for port $port to be freed (${i}s)..." + sleep 1 + done +done + +echo ">>> Starting Flow emulator forked from mainnet..." +flow emulator --fork mainnet --persist=false > /tmp/flow-emulator.log 2>&1 & +EMULATOR_PID=$! + +# Ensure emulator is killed on EXIT, SIGINT, and SIGTERM +_cleanup() { echo ""; echo "Stopping emulator..."; kill "$EMULATOR_PID" 2>/dev/null || true; remove_fork_accounts; } +trap '_cleanup' EXIT +trap 'echo ""; echo "❌ Unexpected error on line $LINENO — stopping emulator..."; kill "$EMULATOR_PID" 2>/dev/null || true' ERR +trap '_cleanup; exit 1' TERM INT + +# Wait for emulator REST API, and verify it is still running +echo ">>> Waiting for emulator REST API at :8888..." +for i in $(seq 1 60); do + if ! kill -0 "$EMULATOR_PID" 2>/dev/null; then + echo "❌ Emulator process died (PID $EMULATOR_PID). Log:" + tail -20 /tmp/flow-emulator.log + exit 1 + fi + 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" + exit 1 + fi + sleep 1 +done + +# --------------------------------------------------------------------------- +# Steps 1–6: Environment setup (shared with docker setup) +# --------------------------------------------------------------------------- + +run_setup 86400 + +# --------------------------------------------------------------------------- +# Step 7: Provision PYUSD0 collateral from donor account +# --------------------------------------------------------------------------- + +echo "" +echo "=== Step 7: Provision PYUSD0 collateral ===" + +run_txn "Setup admin PYUSD0 vault" \ + ./cadence/tests/transactions/setup_ft_vault.cdc \ + "0x1e4aa0b87d10b141" \ + "EVMVMBridgedToken_99af3eea856556646c98c8b9b2548fe815240750" \ + --compute-limit 9999 + +transfer_token "Transfer 2.0 PYUSD0 from donor ($PYUSD0_DONOR) to admin" \ + "0x1e4aa0b87d10b141" \ + "EVMVMBridgedToken_99af3eea856556646c98c8b9b2548fe815240750" \ + 2.0 "$ADMIN_CADENCE_ADDR" "$PYUSD0_DONOR" + +run_script "Admin PYUSD0 balance" \ + ./cadence/scripts/tokens/get_balance.cdc \ + "$ADMIN_CADENCE_ADDR" \ + "/public/EVMVMBridgedToken_99af3eea856556646c98c8b9b2548fe815240750Receiver" 2>/dev/null || true + +sleep 3 + +# --------------------------------------------------------------------------- +# Step 8: PYUSD0 vault lifecycle (syWFLOWvStrategy) +# --------------------------------------------------------------------------- + +echo "" +echo "=== Step 8: PYUSD0 vault lifecycle (syWFLOWvStrategy) ===" + +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" +sleep 3 + +# --------------------------------------------------------------------------- +# Step 9: Seed FlowALP pool with PYUSD0 for FUSDEVStrategy drawdowns +# --------------------------------------------------------------------------- + +echo "" +echo "=== Step 9: Seed FlowALP pool with PYUSD0 ===" + +# Admin publishes pool capability to donor's inbox +run_txn_as "Publish FlowALP beta cap to PYUSD0 holder" "$FLOWALP_POOL_OWNER" \ + ./lib/FlowALP/cadence/transactions/flow-alp/beta/publish_beta_cap.cdc \ + "0x24263c125b7770e0" --compute-limit 9999 + +# Donor claims the capability from the inbox +run_txn_as "Claim FlowALP beta cap (PYUSD0 holder)" "$PYUSD0_DONOR" \ + ./lib/FlowALP/cadence/transactions/flow-alp/beta/claim_and_save_beta_cap.cdc \ + "0x6b00ff876c299c61" --compute-limit 9999 + +# Donor creates a 1000 PYUSD0 reserve position in the pool +run_txn_as "Create 1000 PYUSD0 reserve position in FlowALP pool" "$PYUSD0_DONOR" \ + ./lib/FlowALP/cadence/transactions/flow-alp/position/create_position.cdc \ + 1000.0 \ + "/storage/EVMVMBridgedToken_99af3eea856556646c98c8b9b2548fe815240750Vault" \ + false --compute-limit 9999 + +# --------------------------------------------------------------------------- +# Step 10: FLOW vault lifecycle (FUSDEVStrategy) +# --------------------------------------------------------------------------- + +echo "" +echo "=== Step 10: FLOW vault lifecycle (FUSDEVStrategy) ===" + +run_txn "Create FLOW yield vault (10.0 FLOW)" \ + ./cadence/transactions/flow-yield-vaults/create_yield_vault.cdc \ + "$FUSDEV_STRATEGY_ID" "$FLOW_VAULT_TYPE" 10.0 --compute-limit 9999 + +FLOW_VAULT_ID=$(get_latest_vault_id) +echo " FLOW vault ID: $FLOW_VAULT_ID" + +run_txn "Deposit 5.0 FLOW to vault $FLOW_VAULT_ID" \ + ./cadence/transactions/flow-yield-vaults/deposit_to_yield_vault.cdc \ + "$FLOW_VAULT_ID" 5.0 --compute-limit 9999 + +run_txn "Withdraw 3.0 FLOW from vault $FLOW_VAULT_ID" \ + ./cadence/transactions/flow-yield-vaults/withdraw_from_yield_vault.cdc \ + "$FLOW_VAULT_ID" 3.0 --compute-limit 9999 + +run_txn "Close FLOW vault $FLOW_VAULT_ID" \ + ./cadence/transactions/flow-yield-vaults/close_yield_vault.cdc \ + "$FLOW_VAULT_ID" --compute-limit 9999 + +echo "✅ FUSDEVStrategy FLOW lifecycle complete" + +# --------------------------------------------------------------------------- +# Done +# --------------------------------------------------------------------------- + +echo "" +echo "========================================================" +echo " ✅ All E2E transactions SEALED successfully!" +echo "========================================================" diff --git a/local/mainnet_fork_common.sh b/local/mainnet_fork_common.sh new file mode 100644 index 00000000..0a857409 --- /dev/null +++ b/local/mainnet_fork_common.sh @@ -0,0 +1,202 @@ +#!/usr/bin/env bash +# mainnet_fork_common.sh — Shared variables and helpers for mainnet-fork scripts. +# Source this file; do not execute directly. + +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" +PYUSD0_EVM="0x99aF3EeA856556646C98c8B9b2548Fe815240750" +SYWFLOWV_EVM="0xCBf9a7753F9D2d0e8141ebB36d99f87AcEf98597" +FUSDEV_EVM="0xd069d989e2F44B70c65347d1853C0c67e10a9F8D" + +PYUSD0_VAULT_TYPE="A.1e4aa0b87d10b141.EVMVMBridgedToken_99af3eea856556646c98c8b9b2548fe815240750.Vault" +FLOW_VAULT_TYPE="A.1654653399040a61.FlowToken.Vault" + +STRATEGY_ID="A.b1d63873c3cc9f79.FlowYieldVaultsStrategiesV2.syWFLOWvStrategy" +COMPOSER_ID="A.b1d63873c3cc9f79.FlowYieldVaultsStrategiesV2.MoreERC4626StrategyComposer" +FUSDEV_STRATEGY_ID="A.b1d63873c3cc9f79.FlowYieldVaultsStrategiesV2.FUSDEVStrategy" +FUSDEV_COMPOSER_ID="A.b1d63873c3cc9f79.FlowYieldVaultsStrategiesV2.MorphoERC4626StrategyComposer" +ISSUER_PATH="/storage/FlowYieldVaultsStrategyV2ComposerIssuer_0xb1d63873c3cc9f79" + +# FLOW_HOST may be set by the caller to add --host to all flow commands. +# When empty (local runs), the default localhost:3569 is used. +_host_args() { + if [ -n "${FLOW_HOST:-}" ]; then + echo "--host $FLOW_HOST" + fi +} + +# _check_sealed DESC RESULT +# Checks whether RESULT contains a sealed, error-free transaction. +# Uses bash [[ ]] matching (not pipes) to avoid SIGPIPE under set -o pipefail +# when grep exits early after finding a match in large output. +# If "SEALED" is missing but a Transaction ID is present, queries the status +# as a fallback (handles cases where the CLI times out before printing status). +_check_sealed() { + local desc="$1" + local result="$2" + + if [[ "$result" == *"Transaction Error"* ]]; then + echo "❌ FAIL: '$desc' (Transaction Error)" + exit 1 + fi + + if [[ "$result" == *"SEALED"* ]]; then + echo "✓ $desc" + return 0 + fi + + # CLI may have timed out before receiving the status — re-query by tx ID. + local tx_id="" + if [[ "$result" =~ ([0-9a-f]{64}) ]]; then + tx_id="${BASH_REMATCH[1]}" + fi + if [ -n "$tx_id" ]; then + echo " Status unclear — querying tx $tx_id ..." + local status + status=$(flow transactions status "$tx_id" \ + --network "$NETWORK" $(_host_args) 2>&1 || true) + echo "$status" + if [[ "$status" == *"Transaction Error"* ]]; then + echo "❌ FAIL: '$desc' (Transaction Error)" + exit 1 + fi + if [[ "$status" == *"SEALED"* ]]; then + echo "✓ $desc" + return 0 + fi + fi + + echo "❌ FAIL: '$desc' (not SEALED)" + exit 1 +} + +run_txn() { + local desc="$1" + shift + echo "" + echo ">>> $desc" + local result + result=$(flow transactions send "$@" \ + --network "$NETWORK" $(_host_args) --signer "$SIGNER" 2>&1 || true) + echo "$result" + _check_sealed "$desc" "$result" +} + +# Like run_txn but with an explicit signer (instead of the default $SIGNER). +run_txn_as() { + local desc="$1" + local signer="$2" + shift 2 + echo "" + echo ">>> $desc" + local result + result=$(flow transactions send "$@" \ + --network "$NETWORK" $(_host_args) --signer "$signer" 2>&1 || true) + echo "$result" + _check_sealed "$desc" "$result" +} + +run_script() { + local desc="$1" + shift + echo "" + echo ">>> [script] $desc" + flow scripts execute "$@" --network "$NETWORK" $(_host_args) 2>&1 +} + +# --------------------------------------------------------------------------- +# Setup steps (shared between docker setup and local e2e) +# --------------------------------------------------------------------------- + +setup_deploy_contracts() { + echo "=== Deploy contracts ===" + flow project deploy --network "$NETWORK" $(_host_args) --update +} + +setup_oracle_threshold() { + local threshold="${1:-31536000}" + echo "" + echo ">>> Extending FlowALP oracle staleThreshold to ${threshold}s (fork sig bypass)" + local result + result=$(flow transactions send \ + ./cadence/tests/transactions/update_flowalp_oracle_threshold.cdc \ + "$threshold" \ + --network "$NETWORK" $(_host_args) \ + --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 ${threshold}s" +} + +setup_configure_strategies() { + echo "" + echo "=== Configure strategies ===" + + 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 "Register syWFLOWvStrategy in FlowYieldVaults factory" \ + ./cadence/transactions/flow-yield-vaults/admin/add_strategy_composer.cdc \ + "$STRATEGY_ID" "$COMPOSER_ID" "$ISSUER_PATH" --compute-limit 9999 +} + +setup_configure_fusdev_strategy() { + echo "" + echo "=== Configure FUSDEVStrategy ===" + + run_txn "Configure FUSDEVStrategy + FLOW collateral" \ + ./cadence/transactions/flow-yield-vaults/admin/upsert_strategy_config.cdc \ + "$FUSDEV_STRATEGY_ID" "$FLOW_VAULT_TYPE" "$FUSDEV_EVM" \ + "[\"$FUSDEV_EVM\",\"$PYUSD0_EVM\",\"$WFLOW_EVM\"]" \ + "[100,3000]" --compute-limit 9999 + + run_txn "Register FUSDEVStrategy in FlowYieldVaults factory" \ + ./cadence/transactions/flow-yield-vaults/admin/add_strategy_composer.cdc \ + "$FUSDEV_STRATEGY_ID" "$FUSDEV_COMPOSER_ID" "$ISSUER_PATH" --compute-limit 9999 +} + +setup_grant_beta_access() { + echo "" + echo "=== Grant beta access ===" + run_txn "Grant beta access to admin (self)" \ + ./cadence/tests/transactions/grant_beta_to_self.cdc +} + +setup_fund_admin_coa() { + echo "" + echo "=== 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 +} + +# Runs all setup steps in order. +# $1 — FlowALP oracle staleThreshold in seconds (default: 31536000 = 1 year). +# The fork emulator uses a fixed block timestamp, so the on-chain price feed +# appears stale to FlowALP's oracle check. Setting a large threshold bypasses +# that check without modifying contract logic. +# - docker setup (setup_mainnet_fork_docker.sh): 31536000 (1 year, permanent) +# - local e2e (e2e_mainnet_fork.sh): 86400 (1 day, short-lived run) +run_setup() { + local threshold="${1:-31536000}" + setup_deploy_contracts + setup_oracle_threshold "$threshold" + setup_configure_strategies + setup_configure_fusdev_strategy + setup_grant_beta_access + setup_fund_admin_coa +} diff --git a/local/setup_mainnet_fork_docker.sh b/local/setup_mainnet_fork_docker.sh new file mode 100755 index 00000000..13114a04 --- /dev/null +++ b/local/setup_mainnet_fork_docker.sh @@ -0,0 +1,24 @@ +#!/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). +# +# 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}" + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +# shellcheck source=./mainnet_fork_common.sh +source "$SCRIPT_DIR/mainnet_fork_common.sh" + +run_setup 31536000 + +echo "" +echo "✅ Mainnet-fork setup complete. Backend is ready."