#!/usr/bin/env bash # Thirdweb / EIP-2470 singleton: simulate full calldata on VM 2103 before any broadcast. # - eth_call -> CREATE2 precomputed address (20 bytes, left-padded in 32-byte word) # - cast estimate -> gas (same as eth_estimateGas) # - getCode on predicted -> should be 0x if not yet deployed # - getTransactionCount -> for operator nonce when planning a real send # # Default mode is DRY-RUN only (no transaction). Use --send to broadcast (requires # CONFIRM_BROADCAST=1 in the environment in addition to PRIVATE_KEY from dotenv). # # Usage (from repo root, operator LAN + load-project-env): # source scripts/lib/load-project-env.sh # export INPUT_FILE=/path/to/one-line-0x-hex # ./scripts/verify/dry-run-thirdweb-singleton-2103.sh # # Optional: INPUT_FILE=... RPC_URL_2103=... GAS_LIM= GAS_BUMP_PCT=20 CAST_NONCE= SKIP_2103_POOL_SSH_CHECK=1 # EIP-1559 (required on 2103): cast send without fees used ~15/1 wei maxFee/priority; node often reports # eth_gasPrice ≈ 1000 wei. Set explicitly: # GAS_MAX_FEE_PER_GAS= (wei) GAS_PRIORITY_FEE_PER_GAS= (wei) — or we derive from baseFee + cast gas-price # Send tuning: ETH_TIMEOUT= (sec, default 600) CAST_ASYNC=1 (return hash, no receipt wait) # Post-mine: verify-singleton-post-deploy-2103.sh 0xPred # # Ref: docs/04-configuration/RPC_ENDPOINTS_MASTER.md (2103 Thirdweb admin core) set -euo pipefail SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" PROJECT_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)" # shellcheck disable=SC1091 source "${PROJECT_ROOT}/scripts/lib/load-project-env.sh" SEND=0 while [[ $# -gt 0 ]]; do case "$1" in --send) SEND=1; shift ;; -h | --help) sed -n '1,30p' "$0" exit 0 ;; *) echo "Unknown arg: $1 (use --help)" >&2; exit 2 ;; esac done if [[ "$SEND" -eq 1 && "${CONFIRM_BROADCAST:-0}" != "1" ]]; then echo "ERROR: --send requires CONFIRM_BROADCAST=1. Run this script with no args for a dry-run only; then:" >&2 echo " CONFIRM_BROADCAST=1 $0 --send" >&2 exit 1 fi # VM 2103 Thirdweb admin — do not inherit generic dotenv RPC_URL (often 2101 @ .211) RPC_2103="${RPC_URL_2103:-http://192.168.11.217:8545}" SINGLETON="${SINGLETON:-0x4e59b44847b379578588920cA78FbF26c0B4956C}" INPUT_FILE="${INPUT_FILE:-/tmp/thirdweb-factory-input-2103.hex}" if [[ -z "${PRIVATE_KEY:-}" ]]; then echo "ERROR: PRIVATE_KEY not set (e.g. from smom-dbis-138/.env via load-project-env.sh)" >&2 exit 1 fi if ! command -v cast &>/dev/null; then echo "ERROR: foundry 'cast' not in PATH" >&2 exit 1 fi if [[ ! -f "$INPUT_FILE" ]]; then echo "ERROR: INPUT_FILE not found: $INPUT_FILE" >&2 echo " Hint: run scripts/verify/decode-singleton-deploy-pending-2103.sh when pool has" >&2 echo " Thirdweb deploys, or set INPUT_FILE= to a one-line 0x… file." >&2 exit 1 fi DATA="$(tr -d ' \n\r' < "$INPUT_FILE")" if [[ "${#DATA}" -lt 10 || "${DATA:0:2}" != "0x" ]]; then echo "ERROR: $INPUT_FILE must be a single line 0x-prefixed hex" >&2 exit 1 fi FROM_ADDR="$(cast wallet address "$PRIVATE_KEY" 2>/dev/null || true)" if [[ -z "$FROM_ADDR" || "$FROM_ADDR" == "0x0000000000000000000000000000000000000000" ]]; then echo "ERROR: could not derive address from PRIVATE_KEY" >&2 exit 1 fi if [[ "$SEND" -eq 1 ]]; then echo "=== 2103 singleton broadcast (CONFIRM_BROADCAST=1) ===" else echo "=== 2103 singleton dry-run (no broadcast) ===" fi echo "RPC: $RPC_2103 (set RPC_URL_2103= to override)" echo "Singleton: $SINGLETON" echo "Input: $INPUT_FILE (${#DATA} hex chars = $(( ( ${#DATA} - 2) / 2 )) bytes data)" echo "From: $FROM_ADDR (cast wallet address of PRIVATE_KEY)" echo "" if ! out=$(cast call -r "$RPC_2103" --from "$FROM_ADDR" "$SINGLETON" --data "$DATA" 2>&1); then echo "eth_call (cast call) failed:" >&2 echo "$out" >&2 exit 1 fi if [[ ${#out} -ge 66 ]]; then PRED=0x${out: -40} elif [[ ${#out} -eq 42 && "$out" == 0x* ]]; then PRED="$out" else PRED="$out" fi echo "eth_call result: $out" if [[ "$PRED" =~ ^0x[0-9a-fA-F]{40}$ ]]; then echo "Predicted CREATE2 address: $PRED" else echo "WARN: could not parse 20-byte address from return (see raw above)" >&2 fi if ! gas=$(cast estimate -r "$RPC_2103" --from "$FROM_ADDR" "$SINGLETON" "$DATA" 2>&1); then echo "eth_estimateGas (cast estimate) failed: $gas" >&2 exit 1 fi echo "eth_estimateGas: $gas" if ! [[ "$gas" =~ ^[0-9]+$ ]]; then echo "ERROR: could not parse gas estimate: $gas" >&2 exit 1 fi GAS_BUMP_PCT="${GAS_BUMP_PCT:-20}" if ! [[ "$GAS_BUMP_PCT" =~ ^[0-9]+$ ]]; then echo "ERROR: GAS_BUMP_PCT must be a non-negative integer, got: $GAS_BUMP_PCT" >&2 exit 1 fi if [[ -z "${GAS_LIM:-}" ]]; then GAS_LIM=$((( gas * (100 + GAS_BUMP_PCT) + 99) / 100)) echo "suggested gas limit: $GAS_LIM (estimate * (100+${GAS_BUMP_PCT})/100; set GAS_LIM= to override, GAS_BUMP_PCT= to retune headroom)" else if ! [[ "$GAS_LIM" =~ ^[0-9]+$ ]]; then echo "ERROR: GAS_LIM must be a non-negative integer, got: $GAS_LIM" >&2 exit 1 fi echo "using GAS_LIM=$GAS_LIM (overrides headroom from estimate $gas)" fi # ---- EIP-1559 fee hint from RPC (cast send defaults are unsafe on this chain) ---- base_fee_per_gas="${BASE_FEE_PER_GAS:-$(cast block latest --field baseFeePerGas -r "$RPC_2103" 2>/dev/null || echo 0)}" sugg_wei="${GAS_SUGGEST_WEI:-$(cast gas-price -r "$RPC_2103" 2>/dev/null || echo 0)}" [[ "$base_fee_per_gas" =~ ^[0-9]+$ ]] || base_fee_per_gas=0 if ! [[ "$sugg_wei" =~ ^[0-9]+$ && "$sugg_wei" -ge 1 ]]; then sugg_wei=1000 echo "WARN: could not read eth_gasPrice; using sugg_wei=$sugg_wei" >&2 fi # Max fee: at least 1.2x suggested; override in wei with GAS_MAX_FEE_PER_GAS if [[ -n "${GAS_MAX_FEE_PER_GAS:-}" ]]; then if ! [[ "$GAS_MAX_FEE_PER_GAS" =~ ^[0-9]+$ ]]; then echo "ERROR: GAS_MAX_FEE_PER_GAS must be integer wei, got: $GAS_MAX_FEE_PER_GAS" >&2 exit 1 fi max_fee_per_gas="$GAS_MAX_FEE_PER_GAS" else max_fee_per_gas=$(((sugg_wei * 12 + 9) / 10)) fi # Priority: (sugg - base), min 1; override with GAS_PRIORITY_FEE_PER_GAS if [[ -n "${GAS_PRIORITY_FEE_PER_GAS:-}" ]]; then if ! [[ "$GAS_PRIORITY_FEE_PER_GAS" =~ ^[0-9]+$ ]]; then echo "ERROR: GAS_PRIORITY_FEE_PER_GAS must be integer wei, got: $GAS_PRIORITY_FEE_PER_GAS" >&2 exit 1 fi priority_fee_per_gas="$GAS_PRIORITY_FEE_PER_GAS" else t=$((sugg_wei - base_fee_per_gas)) if [[ "$t" -lt 1 ]]; then t=1; fi priority_fee_per_gas=$t fi need_max=$((base_fee_per_gas + priority_fee_per_gas)) if [[ "$max_fee_per_gas" -lt "$need_max" ]]; then max_fee_per_gas=$(((need_max * 12 + 9) / 10)) fi echo "EIP-1559 (from RPC): baseFeePerGas=$base_fee_per_gas wei eth_gasPrice(sugg)=$sugg_wei wei" echo " would send with: --gas-price $max_fee_per_gas (maxFeePerGas wei) --priority-gas-price $priority_fee_per_gas (priority wei)" echo " (set GAS_MAX_FEE_PER_GAS / GAS_PRIORITY_FEE_PER_GAS in wei to override; see header comment)" if [[ -n "$PRED" && "$PRED" =~ ^0x[0-9a-fA-F]{40}$ ]]; then c=$(cast code -r "$RPC_2103" "$PRED" 2>/dev/null || true) if [[ -n "$c" && "$c" != "0x" && "$c" != "0x0" ]]; then clen=$(((${#c} - 2) / 2)) echo "getCode($PRED) on 2103: $clen byte(s) ${c:0:22}…" else echo "getCode($PRED) on 2103: empty (not yet deployed; a send would create runtime if pool/gas allow)" fi fi nonce_l=$(cast nonce -r "$RPC_2103" --block latest "$FROM_ADDR" 2>&1) || true nonce_p=$(cast nonce -r "$RPC_2103" --block pending "$FROM_ADDR" 2>&1) || true echo "nonce (latest): $nonce_l" echo "nonce (pending): $nonce_p" if [[ ! "$nonce_l" =~ ^[0-9]+$ || ! "$nonce_p" =~ ^[0-9]+$ ]]; then echo "WARN: could not parse both nonces (RPC error?); for --send set CAST_NONCE= or fix RPC" >&2 else if [[ "$nonce_l" -lt "$nonce_p" ]]; then echo "NOTE: mempool in-flight (latest=$nonce_l < pending=$nonce_p) — a **new** first tx reuses/replaces" echo " nonce $nonce_l with higher maxFee+priority, not nonce $nonce_p. Default --send will use $nonce_l unless CAST_NONCE= is set." else echo "next send nonce: $nonce_p (no gap between latest and pending; passed to cast send as --nonce)" fi fi echo "" if [[ "$SEND" -eq 1 ]]; then if ! [[ "$nonce_p" =~ ^[0-9]+$ && "$nonce_l" =~ ^[0-9]+$ ]]; then echo "ERROR: could not read both nonces (latest+pending) for $FROM_ADDR" >&2 exit 1 fi if [[ -n "${CAST_NONCE:-}" ]]; then if [[ ! "$CAST_NONCE" =~ ^[0-9]+$ ]]; then echo "ERROR: CAST_NONCE must be a non-negative integer, got: $CAST_NONCE" >&2 exit 1 fi NONCE_USE="$CAST_NONCE" echo "using CAST_NONCE=$NONCE_USE" elif [[ "$nonce_p" -gt "$nonce_l" ]]; then NONCE_USE="$nonce_l" echo "using --nonce $NONCE_USE (mempool: replace / bump same in-flight; pending counter was $nonce_p)" else NONCE_USE="$nonce_p" echo "using --nonce $NONCE_USE (no extra in-flight per latest vs pending)" fi echo "Broadcasting: --gas-price $max_fee_per_gas --priority-gas-price $priority_fee_per_gas --nonce $NONCE_USE --gas-limit $GAS_LIM" export ETH_TIMEOUT="${ETH_TIMEOUT:-600}" send_args=(cast send -r "$RPC_2103" --private-key "$PRIVATE_KEY" --nonce "$NONCE_USE" --gas-limit "$GAS_LIM" --gas-price "$max_fee_per_gas" --priority-gas-price "$priority_fee_per_gas" "$SINGLETON" "$DATA") if [[ "${CAST_ASYNC:-0}" == "1" ]]; then send_args+=(--async) echo "ETH_TIMEOUT=$ETH_TIMEOUT (CAST_ASYNC=1, no wait for receipt)" else echo "ETH_TIMEOUT=$ETH_TIMEOUT (receipt wait; set CAST_ASYNC=1 for hash only)" fi if ! send_out=$("${send_args[@]}" 2>&1); then echo "cast send failed:" >&2 echo "$send_out" >&2 exit 1 fi echo "$send_out" if [[ -n "$PRED" && "$PRED" =~ ^0x[0-9a-fA-F]{40}$ ]]; then echo "" echo "=== post-deploy (predicted address + admin hint; not the signer) ===" if [[ -x "${SCRIPT_DIR}/verify-singleton-post-deploy-2103.sh" ]]; then export RPC_URL_2103="$RPC_2103" export DEPLOY_TX_SIGNER="$FROM_ADDR" "${SCRIPT_DIR}/verify-singleton-post-deploy-2103.sh" "$PRED" || true fi fi echo "" echo "If the tx is still pending, re-run: DEPLOY_TX_SIGNER=$FROM_ADDR ${SCRIPT_DIR}/verify-singleton-post-deploy-2103.sh $PRED" exit 0 else echo "Dry-run complete. No transaction sent." if [[ -z "${SKIP_2103_POOL_SSH_CHECK:-}" ]]; then echo "Pool (live, optional): ${PROJECT_ROOT}/scripts/verify/verify-2103-besu-txpool-config.sh" fi echo " (or: ${PROJECT_ROOT}/scripts/maintenance/apply-2103-thirdweb-strict-tx-pool.sh --value 1 if the live LXC has 0)" echo "To broadcast after review: CONFIRM_BROADCAST=1 $0 --send" exit 0 fi