diff --git a/.agents/commands/deploy-network.md b/.agents/commands/deploy-network.md new file mode 100644 index 000000000..0a15ddb43 --- /dev/null +++ b/.agents/commands/deploy-network.md @@ -0,0 +1,154 @@ +--- +name: deploy-network +description: Brings a brand-new network fully on-chain by driving `deployAllContracts.sh` (scriptMaster use case 3) end to end — CREATE3 factory, Safe, diamond, core + non-core facets, periphery, whitelist sync, wallet funding, health check — one stage at a time so it survives an agent's per-command time limit and needs no interactive terminal. Use after `add-network` has landed the config (networks.json, foundry.toml, target state) and the deployer is funded, when the user wants to "deploy the new network", "bring up on-chain", "run the full deployment for ", or "deploy all contracts to ". This is the NEW-NETWORK bootstrap path and it runs production in direct-to-diamond mode (`SEND_PROPOSALS_DIRECTLY_TO_DIAMOND=true`) because the Safe/timelock does not own the diamond yet. It stops after stage 11 (health check) and never performs stage 12 (ownership transfer) — that stays a deliberate step. NOT for deploying a single contract to existing networks (use `deploy-contract`), NOT for upgrading facets on live Safe-owned networks (use `multisig-rollout`), and NOT for Tron (`tron`/`tronshasta` — Foundry has no Tron support). Requires Foundry, gh, and `MONGODB_URI` in `.env`. +usage: /deploy-network --production [--from-stage N] [--to-stage M] +--- + +# Deploy Network (LI.FI Contracts) + +Drives a full new-network bring-up via the `deployAllContracts` function +(`script/deploy/deployAllContracts.sh`, scriptMaster use case 3), **one stage per fresh shell**. +Every stage is idempotent (CREATE3 → deterministic addresses + `diamondCut`), so a stage that is +interrupted or re-run resumes instead of duplicating work. This lets an agent complete a deploy +that runs far longer than a single command's time budget, with no TTY, by invoking the script +once per stage with the stage range and non-interactive mode preset in the environment. + +## When to use this vs other deploy skills + +| Situation | Skill | +|---|---| +| Network *config* (networks.json, foundry.toml, target state) | **`add-network`** (run FIRST) | +| Bring a **new** network fully on-chain | **this skill** | +| Deploy/redeploy a **single** contract to existing network(s) | **`deploy-contract`** | +| Upgrade facets/periphery on **existing, Safe-owned** networks | **`multisig-rollout`** | +| Ownership transfer / post-rollout completion | deliberate (`finish-rollout` / stage 12) | + +## Hard rails + +- **New-network bootstrap only.** Sets `SEND_PROPOSALS_DIRECTLY_TO_DIAMOND=true` (bypasses the + Safe) — legitimate ONLY before ownership has moved to the Safe/timelock. If the network is + already Safe-owned, stop — use `multisig-rollout`. +- **Never runs stage 12 (ownership transfer).** Bounded to stage 11. Ownership transfer is a + deliberate, reviewed step after the health check is clean. +- **Never edits `.env` silently.** Production flags + any path/verify fixes are shared, symlinked + state — set them with the user's explicit go, announce it, restore afterward. +- **Tron is out of scope** (no Foundry/CREATE3 support). + +## Execution mechanics (read before running anything) + +- **Run every command under `bash -c`.** The agent shell is zsh; the deploy scripts use bash-isms + (`read -a`, associative arrays). Under zsh you get `read: bad option: -a` and silent mis-parses. +- **PATH**: prepend `export PATH="$HOME/.foundry/bin:$HOME/.bun/bin:$PATH"`. +- **`dangerouslyDisableSandbox: true`** for anything doing network I/O (deploy, verify, cast, mongo). +- **Env overrides that SURVIVE the script's `.env` re-source** (not set in `.env`): `START_STAGE`, + `END_STAGE`, `NON_INTERACTIVE`. Set these inline. Anything already in `.env` (`PRODUCTION`, + `VERIFY_CONTRACTS`, `GLOBAL_FILE_PATH`, `DO_NOT_VERIFY_IN_THESE_NETWORKS`) is re-sourced by the + script and CANNOT be overridden by an export — edit the `.env` file for those. +- **Output is huge** (bytecode dumps on single lines) — pipe to a log file and `grep` for markers + (`STAGE N completed`, `successfully verified`, `Failed`, `Error:`), do NOT tail raw. +- **10-min SIGTERM**: the agent's Bash tool hard-kills any command (incl. backgrounded) at ~10min. + Run one stage per shell; a stage that neither completes nor progresses across a re-run cap (~5) + → stop and surface, don't loop. + +## Phase 0 — Preflight (do IN THIS ORDER; report, don't silently fix) + +1. **`add-network` landed**: network present in `config/networks.json`, `foundry.toml` + (`[rpc_endpoints]` + `[etherscan]`), and `script/deploy/_targetState.json`. +2. **Submodules**: `git submodule update --init --recursive` (fresh worktrees have none → forge + import resolution fails). +3. **node_modules**: symlink the main checkout's `node_modules` (avoids `bun.lock` churn) or + `bun install` — needed for TS tooling + lint-staged. +4. **typechain**: `bun typechain:incremental` — TS deploy helpers (`deploy-safe.ts`, etc.) crash + `ERR_MODULE_NOT_FOUND … /typechain` without it. `forge build` does NOT generate it. +5. **Fresh build**: `forge clean && forge build` (CREATE3 salt derives from `out/`). +6. **`GLOBAL_FILE_PATH` sanity**: `source .env` and assert `GLOBAL_FILE_PATH` resolves to an + existing file from repo root (framework default `config/global.json`). A stale relative value + like `./../config/global.json` silently breaks stage 2. Empty `NETWORKS_JSON_FILE_PATH` is fine + (falls back to default). +7. **Verifier endpoint probe**: for blockscout networks the verify API is often a SEPARATE + subdomain (e.g. `blockscout-api.` not the explorer UI `blockscout.`). Probe the + `explorerApiUrl` returns JSON, not HTML — a wrong URL makes verification 404, which the deploy + misreads as a gateway timeout and RETRIES WITH BACKOFF, throttling everything. +8. **RPC**: prefer a **premium RPC** (Alchemy/Quicknode). Public RPCs can be ~1.4s/call → 30-60s + of forking PER contract; a premium RPC is ~2x+ faster. Add via `bun add-network-rpc --network + --rpc-url ` then `bun fetch-rpcs` (RPC Mongo is separate/unprotected — no + lifi-connect needed). Stage 1 auto-adds the `networks.json` public rpcUrl if none is set. +9. **corePeriphery ↔ target-state reconciliation** (prevents silent gaps): diff + `config/global.json` `corePeriphery` against the parsed target-state periphery. Flag + (a) corePeriphery items MISSING from the target state → they won't deploy (add to the sheet), + and (b) deprecated items still in corePeriphery → they'll fail the health check forever (remove + from corePeriphery). Account for legitimate per-chain skips: GasZip* (no gaszip config), + Permit2Proxy (no Permit2 on chain). +10. **Version currency**: check the target-state parse warnings for "differs from current" — a + stale sheet deploys old versions. Critically, ERC20Proxy must be **>= 1.2.0** or the Executor + is NOT pre-authorized (forces a manual `setAuthorizedCaller`). Update the sheet + re-parse if stale. +11. **Deployer funded** (stage 1 aborts on 0). Pauser/Dev are funded automatically at stage 9 via + `NON_INTERACTIVE` defaults — do NOT pre-require them. +12. **Production flags**: edit `.env` → `PRODUCTION=true` and `SEND_PROPOSALS_DIRECTLY_TO_DIAMOND=true`. + +## The stages + +| # | Stage | Notes | +|---|---|---| +| 1 | Setup + CREATE3Factory (+ Safe on production) | RPC self-heals here | +| 2 | Deploy core facets | GasZipFacet skipped if no gaszip config | +| 3 | Deploy diamond + cut in core facets | | +| 4 | Approve refund wallet | | +| 5 | Deploy non-core facets + cut | empty for a core-only setup | +| 6 | Deploy periphery | Permit2Proxy/GasZipPeriphery skipped if unconfigured | +| 7 | Register periphery in diamond | | +| 8 | Whitelist sync | writes `config/whitelist.json` (deploy-generated, commit it) | +| 9 | Fund Pauser + Dev | `NON_INTERACTIVE` uses computed defaults | +| 10 | Verify Executor authorization | "already authorized" when ERC20Proxy >= 1.2.0 | +| 11 | Health check | bounded stop here | +| 12 | Ownership transfer | **skipped by this skill** | + +## Phase 1 — Drive the stages + +Deploy with **verification OFF** for speed (verify in Phase 2 instead). One stage per shell: + +```bash +export PATH="$HOME/.foundry/bin:$HOME/.bun/bin:$PATH" +cd +START_STAGE= END_STAGE= NON_INTERACTIVE=true VERIFY_CONTRACTS=false \ + bash -c 'source script/deploy/deployAllContracts.sh; deployAllContracts production' \ + 2>&1 | sed 's/\x1b\[[0-9;]*m//g' > /tmp/stage.log +grep -c "STAGE completed" /tmp/stage.log # 1 = advance; 0 = inspect/re-run +``` + +(`VERIFY_CONTRACTS=false` is in `.env`, so to truly disable inline verification either edit `.env` +for the run or rely on the Phase-2 sweep; deploying verification-off avoids inline `--watch` +blocking and Blockscout indexing-lag failures.) + +- **Marker present** → advance to N+1. **Absent** → re-run the same stage (idempotent; heavy + stage 5 may need a few re-runs). Cap ~5; then stop + surface with the log. +- **Transient RPC/DNS**: a stage may fail once with `could not instantiate forked environment … + dns error`; a plain re-run usually succeeds. Retry before deep diagnosis. +- Runs auto-stop after stage 11 (`END_STAGE < 12`), leaving the deployer as diamond owner — the + correct resumable pre-ownership-transfer state. + +## Phase 2 — Verify all contracts (parallel, rate-limit-aware) + +Run ONCE after all deploys (Blockscout has had time to index → avoids "address is not a smart +contract" races). The master log is in **MongoDB** — `bun mongo-logs:sync` refreshes the local +cache (`_deployments_log_file.json`) so logged `constructorArgs`/`solc`/`evm` are available. + +Use `verifyNetworkContractsParallel ` (ships with this skill): verifies all +unverified contracts for the network concurrently (capped at `MAX_CONCURRENT_JOBS`), with +**Blockscout rate-limit backoff** (429/gateway → exponential retry), then pushes `verified:true` +back to MongoDB. Confirm with `bun mongo-logs:query`. Re-run to mop up any left by rate limits. + +## Phase 3 — Land results + +- Commit `deployments/.json`, `.diamond.json`, and the stage-8 `config/whitelist.json` + additions → turns `check-new-network-health` green. +- Restore `.env` (`PRODUCTION=false`, `SEND_PROPOSALS_DIRECTLY_TO_DIAMOND=` empty; keep + `GLOBAL_FILE_PATH` fixed). +- Health check will still note, by design: GasZip/Permit2 (unconfigured/N-A) and diamond ownership + (stage 12 deferred). Those are expected, not failures. + +## Hand-offs + +- **Ownership transfer** (stage 12) → deliberate step / `finish-rollout` once the health check + shows only ownership errors. +- **Bridge integrations** → their own follow-up deploys. diff --git a/.claude/skills/deploy-network/SKILL.md b/.claude/skills/deploy-network/SKILL.md new file mode 120000 index 000000000..b5dc9ad3d --- /dev/null +++ b/.claude/skills/deploy-network/SKILL.md @@ -0,0 +1 @@ +../../../.agents/commands/deploy-network.md \ No newline at end of file diff --git a/.cursor/commands/deploy-network.md b/.cursor/commands/deploy-network.md new file mode 120000 index 000000000..d0a703f85 --- /dev/null +++ b/.cursor/commands/deploy-network.md @@ -0,0 +1 @@ +../../.agents/commands/deploy-network.md \ No newline at end of file diff --git a/script/deploy/deployAllContracts.sh b/script/deploy/deployAllContracts.sh index cee52fd86..07d706fcf 100755 --- a/script/deploy/deployAllContracts.sh +++ b/script/deploy/deployAllContracts.sh @@ -5,6 +5,7 @@ deployAllContracts() { # load required resources source script/helperFunctions.sh + source script/deploy/deploySingleContract.sh source script/deploy/deployAndStoreCREATE3Factory.sh source script/deploy/deployCoreFacets.sh source script/deploy/deployFacetAndAddToDiamond.sh @@ -52,46 +53,61 @@ deployAllContracts() { fi fi - # Ask user where to start the deployment process - echo "Which stage would you like to start from?" - START_FROM=$( - gum choose \ - "1) Initial setup and CREATE3Factory deployment" \ - "2) Deploy core facets" \ - "3) Deploy diamond and update with core facets" \ - "4) Set approval for refund wallet" \ - "5) Deploy non-core facets and add to diamond" \ - "6) Deploy periphery contracts" \ - "7) Add periphery to diamond" \ - "8) Update whitelist.json and execute sync whitelist script" \ - "9) Fund PauserWallet and DevWallet" \ - "10) Verify ERC20Proxy authorization" \ - "11) Run health check only" \ - "12) Ownership transfer to timelock (production only)" - ) - - # Extract the stage number from the selection (e.g. "12) ...") - # Important: do NOT substring-match "1)" as it would also match "10)", "11)", "12)". - if [[ "$START_FROM" =~ ^([0-9]+)\) ]]; then - START_STAGE="${BASH_REMATCH[1]}" + # Stage range selection. + # Interactive use: ask via gum, run from the chosen stage through stage 12. + # Automated use (e.g. the deploy-network skill): preset START_STAGE (and optionally + # END_STAGE) in the environment to bound the run and skip the prompt entirely. This + # lets a caller run one stage per invocation to stay under process time limits; every + # stage is idempotent (CREATE3 + diamondCut) so a bounded/re-run range is safe. + END_STAGE="${END_STAGE:-12}" + if [[ -n "${START_STAGE:-}" ]]; then + echo "[info] START_STAGE=$START_STAGE END_STAGE=$END_STAGE preset in environment; skipping interactive stage selection" else - error "invalid selection: $START_FROM - exiting script now" - exit 1 + echo "Which stage would you like to start from?" + START_FROM=$( + gum choose \ + "1) Initial setup and CREATE3Factory deployment" \ + "2) Deploy core facets" \ + "3) Deploy diamond and update with core facets" \ + "4) Set approval for refund wallet" \ + "5) Deploy non-core facets and add to diamond" \ + "6) Deploy periphery contracts" \ + "7) Add periphery to diamond" \ + "8) Update whitelist.json and execute sync whitelist script" \ + "9) Fund PauserWallet and DevWallet" \ + "10) Verify ERC20Proxy authorization" \ + "11) Run health check only" \ + "12) Ownership transfer to timelock (production only)" + ) + + # Extract the stage number from the selection (e.g. "12) ...") + # Important: do NOT substring-match "1)" as it would also match "10)", "11)", "12)". + if [[ "$START_FROM" =~ ^([0-9]+)\) ]]; then + START_STAGE="${BASH_REMATCH[1]}" + else + error "invalid selection: $START_FROM - exiting script now" + exit 1 + fi fi if [[ "$START_STAGE" -lt 1 || "$START_STAGE" -gt 12 ]]; then - error "invalid selection (stage out of range): $START_FROM - exiting script now" + error "invalid START_STAGE (out of range 1-12): $START_STAGE - exiting script now" + exit 1 + fi + + if [[ "$END_STAGE" -lt "$START_STAGE" || "$END_STAGE" -gt 12 ]]; then + error "invalid END_STAGE ($END_STAGE): must be between START_STAGE ($START_STAGE) and 12 - exiting script now" exit 1 fi - echo "Starting from stage $START_STAGE: $START_FROM" + echo "Starting from stage $START_STAGE through stage $END_STAGE" echo "" # since we only support mutable diamonds, no need to ask user to select diamond type local DIAMOND_CONTRACT_NAME="LiFiDiamond" # Stage 1: Initial setup and CREATE3Factory deployment - if [[ $START_STAGE -le 1 ]]; then + if [[ $START_STAGE -le 1 && $END_STAGE -ge 1 ]]; then echo "[info] >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> STAGE 1: Initial setup and CREATE3Factory deployment" # add RPC URL to MongoDB (only if not already in .env) @@ -154,7 +170,7 @@ deployAllContracts() { fi # Stage 2: Deploy core facets - if [[ $START_STAGE -le 2 ]]; then + if [[ $START_STAGE -le 2 && $END_STAGE -ge 2 ]]; then echo "" echo "[info] >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> STAGE 2: Deploy core facets" @@ -166,7 +182,7 @@ deployAllContracts() { fi # Stage 3: Deploy diamond and update with core facets - if [[ $START_STAGE -le 3 ]]; then + if [[ $START_STAGE -le 3 && $END_STAGE -ge 3 ]]; then echo "" echo "[info] >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> STAGE 3: Deploy diamond and update with core facets" @@ -203,7 +219,7 @@ deployAllContracts() { fi # Stage 4: Set approval for refund wallet - if [[ $START_STAGE -le 4 ]]; then + if [[ $START_STAGE -le 4 && $END_STAGE -ge 4 ]]; then echo "" echo "[info] >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> STAGE 4: Set approval for refund wallet" @@ -218,7 +234,7 @@ deployAllContracts() { fi # Stage 5: Deploy non-core facets and add to diamond - if [[ $START_STAGE -le 5 ]]; then + if [[ $START_STAGE -le 5 && $END_STAGE -ge 5 ]]; then echo "" echo "[info] >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> STAGE 5: Deploy non-core facets and add to diamond" @@ -256,7 +272,7 @@ deployAllContracts() { fi # Stage 6: Deploy periphery contracts - if [[ $START_STAGE -le 6 ]]; then + if [[ $START_STAGE -le 6 && $END_STAGE -ge 6 ]]; then echo "" echo "[info] >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> STAGE 6: Deploy periphery contracts" @@ -267,7 +283,7 @@ deployAllContracts() { fi # Stage 7: Add periphery to diamond - if [[ $START_STAGE -le 7 ]]; then + if [[ $START_STAGE -le 7 && $END_STAGE -ge 7 ]]; then echo "" echo "[info] >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> STAGE 7: Add periphery to diamond" @@ -280,7 +296,7 @@ deployAllContracts() { fi # Stage 8: Update whitelist.json and execute sync whitelist script - if [[ $START_STAGE -le 8 ]]; then + if [[ $START_STAGE -le 8 && $END_STAGE -ge 8 ]]; then echo "" echo "[info] >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> STAGE 8: Update whitelist.json and execute sync whitelist script" @@ -293,7 +309,7 @@ deployAllContracts() { fi # Stage 9: Fund PauserWallet and DevWallet - if [[ $START_STAGE -le 9 ]]; then + if [[ $START_STAGE -le 9 && $END_STAGE -ge 9 ]]; then echo "" echo "[info] >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> STAGE 9: Fund PauserWallet and DevWallet" @@ -348,8 +364,13 @@ deployAllContracts() { DEFAULT_FUND_AMOUNT=$FALLBACK_FUND_AMOUNT warning "could not estimate pause cost for $NETWORK; falling back to default $DEFAULT_FUND_AMOUNT wei" fi - echo "PauserWallet balance is 0. Enter wei to send to $PAUSER_WALLET_ADDRESS (edit or press Enter to confirm default):" - FUNDING_AMOUNT=$(gum input --value "$DEFAULT_FUND_AMOUNT" --placeholder "wei amount" --width 40) + if [[ "${NON_INTERACTIVE:-}" == "true" ]]; then + echo "[info] NON_INTERACTIVE: funding PauserWallet with computed default $DEFAULT_FUND_AMOUNT wei" + FUNDING_AMOUNT="$DEFAULT_FUND_AMOUNT" + else + echo "PauserWallet balance is 0. Enter wei to send to $PAUSER_WALLET_ADDRESS (edit or press Enter to confirm default):" + FUNDING_AMOUNT=$(gum input --value "$DEFAULT_FUND_AMOUNT" --placeholder "wei amount" --width 40) + fi FUNDING_AMOUNT="${FUNDING_AMOUNT:-$DEFAULT_FUND_AMOUNT}" # Validate that FUNDING_AMOUNT is a numeric value @@ -383,8 +404,13 @@ deployAllContracts() { if [[ "$BALANCE" == "0" ]]; then local DEFAULT_FUND_AMOUNT=2000000000000000 - echo "DevWallet balance is 0. Enter wei to send to $DEV_WALLET_ADDRESS (edit or press Enter to confirm default):" - FUNDING_AMOUNT=$(gum input --value "$DEFAULT_FUND_AMOUNT" --placeholder "wei amount" --width 40) + if [[ "${NON_INTERACTIVE:-}" == "true" ]]; then + echo "[info] NON_INTERACTIVE: funding DevWallet with default $DEFAULT_FUND_AMOUNT wei" + FUNDING_AMOUNT="$DEFAULT_FUND_AMOUNT" + else + echo "DevWallet balance is 0. Enter wei to send to $DEV_WALLET_ADDRESS (edit or press Enter to confirm default):" + FUNDING_AMOUNT=$(gum input --value "$DEFAULT_FUND_AMOUNT" --placeholder "wei amount" --width 40) + fi FUNDING_AMOUNT="${FUNDING_AMOUNT:-$DEFAULT_FUND_AMOUNT}" # Validate that FUNDING_AMOUNT is a numeric value @@ -405,7 +431,7 @@ deployAllContracts() { fi # Stage 10: Verify ERC20Proxy authorization - if [[ $START_STAGE -le 10 ]]; then + if [[ $START_STAGE -le 10 && $END_STAGE -ge 10 ]]; then echo "" echo "[info] >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> STAGE 10: Verify ERC20Proxy authorization" @@ -426,7 +452,7 @@ deployAllContracts() { fi # Stage 11: Run health check only - if [[ $START_STAGE -le 11 ]]; then + if [[ $START_STAGE -le 11 && $END_STAGE -ge 11 ]]; then echo "" echo "[info] >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> STAGE 11: Run health check only" bun run healthcheck --network "$NETWORK" --environment "$ENVIRONMENT" @@ -435,7 +461,13 @@ deployAllContracts() { # Pause and ask user if they want to continue with ownership transfer. # Testnets keep deployerWallet as the owner, so Stage 12 is skipped entirely. - if [[ "$ENVIRONMENT" == "production" ]] && ! isTestnetNetwork "$NETWORK"; then + # Bounded runs (END_STAGE < 12) stop here without prompting — ownership transfer is + # a deliberate step outside an automated bring-up. + if [[ "$END_STAGE" -lt 12 ]]; then + echo "[info] END_STAGE=$END_STAGE: stopping after stage 11 (ownership transfer not in requested range)" + echo "[info] <<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<< deployAllContracts completed" + return + elif [[ "$ENVIRONMENT" == "production" ]] && ! isTestnetNetwork "$NETWORK"; then echo "" echo "Health check completed. Do you want to continue with ownership transfer to timelock?" echo "This should only be done if the health check shows only diamond ownership errors." @@ -454,7 +486,7 @@ deployAllContracts() { fi # Stage 12: Ownership transfer to timelock (production only; skipped on testnet) - if [[ $START_STAGE -le 12 ]]; then + if [[ $START_STAGE -le 12 && $END_STAGE -ge 12 ]]; then if [[ "$ENVIRONMENT" == "production" ]] && ! isTestnetNetwork "$NETWORK"; then echo "" echo "[info] >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> STAGE 12: Ownership transfer to timelock (production only)" diff --git a/script/deploy/deploySingleContract.sh b/script/deploy/deploySingleContract.sh index cd3a06dab..d71d970d1 100755 --- a/script/deploy/deploySingleContract.sh +++ b/script/deploy/deploySingleContract.sh @@ -263,8 +263,23 @@ deploySingleContract() { # execute script attempts=1 ADDRESS_COLLISION_DETECTED=false + # Reset before the loop so a value leaked from a previous contract can never wrongly trigger the + # short-circuit below (deploySingleContract is called in a loop by deployFacetAndAddToDiamond). + ADDRESS="" + + # Short-circuit already-deployed contracts (opt out with SKIP_IF_ALREADY_DEPLOYED=false). + # DEPLOYSALT includes the compiled bytecode, so the predicted CREATE3 address is version-specific: + # if it already has code (NEW_DEPLOYMENT=="true"), this exact contract+version is already deployed + # and running the forge script would only fork the RPC (~dozens of calls) to no-op. Reuse the + # deployed address and skip the loop; the log-matching further below supplies the constructor args. + # A version bump changes the bytecode -> a different salt/address with no code -> deploys normally. + # (Only the non-zkEVM CREATE3 path sets NEW_DEPLOYMENT, so this naturally never fires on zkEVM.) + if [[ "${SKIP_IF_ALREADY_DEPLOYED:-true}" == "true" && "$NEW_DEPLOYMENT" == "true" ]]; then + echo "[info] $CONTRACT already deployed at $CONTRACT_ADDRESS (version-specific CREATE3 salt) - skipping forge deployment" + ADDRESS="$CONTRACT_ADDRESS" + fi - while [ $attempts -le "$MAX_ATTEMPTS_PER_CONTRACT_DEPLOYMENT" ]; do + while [ $attempts -le "$MAX_ATTEMPTS_PER_CONTRACT_DEPLOYMENT" ] && [[ -z "$ADDRESS" ]]; do echo "[info] trying to deploy $CONTRACT now - attempt ${attempts} (max attempts: $MAX_ATTEMPTS_PER_CONTRACT_DEPLOYMENT) " # ensure that gas price is below maximum threshold (for mainnet only) diff --git a/script/helperFunctions.sh b/script/helperFunctions.sh index e69dd656e..aabb59653 100755 --- a/script/helperFunctions.sh +++ b/script/helperFunctions.sh @@ -2314,7 +2314,8 @@ function verifyContract() { # Check if command failed with non-zero exit code if [ $VERIFY_EXIT_CODE -ne 0 ]; then # Check for specific error types that should trigger retry with longer delay - if echo "$VERIFY_OUTPUT" | grep -qi "504\|Gateway Time-out\|timeout\|Failed to obtain contract ABI"; then + # (includes explorer rate-limiting, which parallel verification is prone to hit) + if echo "$VERIFY_OUTPUT" | grep -qi "504\|Gateway Time-out\|timeout\|Failed to obtain contract ABI\|429\|rate limit\|too many requests"; then warning "API timeout or gateway error detected. This may be temporary - will retry with longer delay..." error "Verification command failed with exit code $VERIFY_EXIT_CODE (API timeout/gateway error)" if [ -n "$VERIFY_OUTPUT" ]; then @@ -2544,6 +2545,107 @@ function verifyAllUnverifiedContractsInLogFile() { echo "[info] done (verified contracts: $COUNTER)" } +function verifyNetworkContractsParallel() { + # Parallel, network-scoped verification sweep with explorer rate-limit backoff. + # Verifies every not-yet-verified contract for ONE network/environment concurrently, then + # updates the deployment log SEQUENTIALLY afterwards (parallel log writes would corrupt it). + # Meant to run AFTER a full deploy (deploy stages with VERIFY_CONTRACTS=false) so the explorer + # has indexed everything. Requires the log to be synced first: `bun mongo-logs:sync`. + # Args: $1 NETWORK, $2 ENVIRONMENT + local NETWORK="$1" + local ENVIRONMENT="$2" + + if [[ -z "$NETWORK" || -z "$ENVIRONMENT" ]]; then + error "verifyNetworkContractsParallel requires NETWORK and ENVIRONMENT" + return 1 + fi + if [ ! -f "$LOG_FILE_PATH" ]; then + error "log file not found at $LOG_FILE_PATH (run 'bun mongo-logs:sync' first)" + return 1 + fi + + # Verification concurrency is intentionally lower than MAX_CONCURRENT_JOBS: block explorers + # rate-limit aggressively. Each verifyContract already retries with backoff on 429/gateway. + local CONCURRENCY=${VERIFICATION_MAX_CONCURRENT_JOBS:-4} + if [[ -n "$MAX_CONCURRENT_JOBS" && "$MAX_CONCURRENT_JOBS" -lt "$CONCURRENCY" ]]; then + CONCURRENCY=$MAX_CONCURRENT_JOBS + fi + + local WORK_DIR + WORK_DIR=$(mktemp -d) + local WORK_LIST="$WORK_DIR/worklist" + : >"$WORK_LIST" + + # Collect all unverified entries for this network/environment into a tab-separated work list. + local CONTRACT + for CONTRACT in $(jq -r 'keys[]' "$LOG_FILE_PATH"); do + jq -e --arg c "$CONTRACT" --arg n "$NETWORK" '.[$c][$n]' "$LOG_FILE_PATH" >/dev/null 2>&1 || continue + local VERSION + for VERSION in $(jq -r --arg c "$CONTRACT" --arg n "$NETWORK" --arg e "$ENVIRONMENT" '.[$c][$n][$e] // {} | keys[]' "$LOG_FILE_PATH" 2>/dev/null); do + local ENTRY + ENTRY=$(jq -r --arg c "$CONTRACT" --arg n "$NETWORK" --arg e "$ENVIRONMENT" --arg v "$VERSION" '.[$c][$n][$e][$v][0]' "$LOG_FILE_PATH") + [[ -z "$ENTRY" || "$ENTRY" == "null" ]] && continue + [[ "$(echo "$ENTRY" | jq -r '.VERIFIED // "false"')" == "true" ]] && continue + local ADDRESS ARGS + ADDRESS=$(echo "$ENTRY" | jq -r '.ADDRESS // empty') + ARGS=$(echo "$ENTRY" | jq -r '.CONSTRUCTOR_ARGS // empty') + [[ -z "$ADDRESS" ]] && continue + printf '%s\t%s\t%s\t%s\n' "$CONTRACT" "$VERSION" "$ADDRESS" "$ARGS" >>"$WORK_LIST" + done + done + + local TOTAL + TOTAL=$(wc -l <"$WORK_LIST" | tr -d ' ') + if [[ "$TOTAL" -eq 0 ]]; then + echo "[info] no unverified contracts for $NETWORK/$ENVIRONMENT" + rm -rf "$WORK_DIR" + return 0 + fi + echo "[info] verifying $TOTAL contract(s) for $NETWORK/$ENVIRONMENT (concurrency: $CONCURRENCY)" + + # Dispatch parallel verification jobs. Each writes only its own result file; no job touches the + # shared log. Small stagger between dispatches to avoid a thundering herd against the explorer. + while IFS=$'\t' read -r CONTRACT VERSION ADDRESS ARGS; do + while [[ $(jobs -r | wc -l | tr -d ' ') -ge $CONCURRENCY ]]; do sleep 1; done + ( + if verifyContract "$NETWORK" "$CONTRACT" "$ADDRESS" "$ARGS" >/dev/null 2>&1; then + echo "ok" >"$WORK_DIR/$CONTRACT.$VERSION.result" + else + echo "fail" >"$WORK_DIR/$CONTRACT.$VERSION.result" + fi + ) & + sleep 2 + done <"$WORK_LIST" + wait + + # Sequentially flip VERIFIED=true in the log for the successes (preserving all other fields). + local OK=0 + local FAILED=() + while IFS=$'\t' read -r CONTRACT VERSION ADDRESS ARGS; do + if [[ "$(cat "$WORK_DIR/$CONTRACT.$VERSION.result" 2>/dev/null)" == "ok" ]]; then + local ENTRY TIMESTAMP OPTIMIZER_RUNS SALT SOLC_VERSION EVM_VERSION ZK_SOLC_VERSION + ENTRY=$(jq -r --arg c "$CONTRACT" --arg n "$NETWORK" --arg e "$ENVIRONMENT" --arg v "$VERSION" '.[$c][$n][$e][$v][0]' "$LOG_FILE_PATH") + TIMESTAMP=$(echo "$ENTRY" | jq -r '.TIMESTAMP // empty') + OPTIMIZER_RUNS=$(echo "$ENTRY" | jq -r '.OPTIMIZER_RUNS // empty') + SALT=$(echo "$ENTRY" | jq -r '.SALT // empty') + SOLC_VERSION=$(echo "$ENTRY" | jq -r '.SOLC_VERSION // empty') + EVM_VERSION=$(echo "$ENTRY" | jq -r '.EVM_VERSION // empty') + ZK_SOLC_VERSION=$(echo "$ENTRY" | jq -r '.ZK_SOLC_VERSION // empty') + logContractDeploymentInfo "$CONTRACT" "$NETWORK" "$TIMESTAMP" "$VERSION" "$OPTIMIZER_RUNS" "$ARGS" "$ENVIRONMENT" "$ADDRESS" "true" "$SALT" "$SOLC_VERSION" "$EVM_VERSION" "$ZK_SOLC_VERSION" + OK=$((OK + 1)) + else + FAILED+=("$CONTRACT ($ADDRESS)") + fi + done <"$WORK_LIST" + + rm -rf "$WORK_DIR" + echo "[info] verified $OK/$TOTAL for $NETWORK/$ENVIRONMENT" + if [[ ${#FAILED[@]} -gt 0 ]]; then + warning "still unverified (explorer rate-limit/indexing lag — re-run to retry): ${FAILED[*]}" + return 1 + fi + return 0 +} function removeFacetFromDiamond() { # read function arguments into variables local DIAMOND_ADDRESS="$1" diff --git a/script/tasks/verifyERC20ProxyAuthorization.sh b/script/tasks/verifyERC20ProxyAuthorization.sh index e194ce5df..ed237cf64 100755 --- a/script/tasks/verifyERC20ProxyAuthorization.sh +++ b/script/tasks/verifyERC20ProxyAuthorization.sh @@ -107,11 +107,23 @@ authorizeExecutorOnZkEvm() { OWNER_BALANCE=$(cast balance "$OWNER" --rpc-url "$RPC_URL") echo "[info] Owner ($OWNER) native balance: $OWNER_BALANCE wei" - if gum confirm "Fund owner with gas for the setAuthorizedCaller tx now?"; then - local DEFAULT_FUND_AMOUNT=1000000000000000 # 0.001 native; one cheap state-setting tx - echo "Enter wei to send to $OWNER (edit or press Enter to confirm default):" + local DEFAULT_FUND_AMOUNT=1000000000000000 # 0.001 native; one cheap state-setting tx + local DO_FUND_OWNER=false + if [[ "${NON_INTERACTIVE:-}" == "true" ]]; then + echo "[info] NON_INTERACTIVE: auto-funding owner with default $DEFAULT_FUND_AMOUNT wei for the setAuthorizedCaller tx" + DO_FUND_OWNER=true + elif gum confirm "Fund owner with gas for the setAuthorizedCaller tx now?"; then + DO_FUND_OWNER=true + fi + + if [[ "$DO_FUND_OWNER" == "true" ]]; then local FUNDING_AMOUNT - FUNDING_AMOUNT=$(gum input --value "$DEFAULT_FUND_AMOUNT" --placeholder "wei amount" --width 40) + if [[ "${NON_INTERACTIVE:-}" == "true" ]]; then + FUNDING_AMOUNT="$DEFAULT_FUND_AMOUNT" + else + echo "Enter wei to send to $OWNER (edit or press Enter to confirm default):" + FUNDING_AMOUNT=$(gum input --value "$DEFAULT_FUND_AMOUNT" --placeholder "wei amount" --width 40) + fi FUNDING_AMOUNT="${FUNDING_AMOUNT:-$DEFAULT_FUND_AMOUNT}" if ! [[ "$FUNDING_AMOUNT" =~ ^[0-9]+$ ]]; then error "Invalid funding amount. Please provide a valid wei amount (numeric value)."