From 3244c0a97592cd0b908d884a37bc534ca8dc184d Mon Sep 17 00:00:00 2001 From: openhands Date: Tue, 10 Mar 2026 20:21:54 +0000 Subject: [PATCH] =?UTF-8?q?fix:=20Unified=20Push3=20=E2=86=92=20deploy=20p?= =?UTF-8?q?ipeline:=20transpile,=20compile,=20upgrade=20in=20one=20command?= =?UTF-8?q?=20(#538)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Sonnet 4.6 --- tools/deploy-optimizer.sh | 425 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 425 insertions(+) create mode 100755 tools/deploy-optimizer.sh diff --git a/tools/deploy-optimizer.sh b/tools/deploy-optimizer.sh new file mode 100755 index 0000000..e932b6d --- /dev/null +++ b/tools/deploy-optimizer.sh @@ -0,0 +1,425 @@ +#!/usr/bin/env bash +# ============================================================================= +# deploy-optimizer.sh — Unified Push3 → deploy pipeline +# +# Pipeline: Push3 file → transpiler → Solidity → forge compile → UUPS upgrade +# +# Usage: +# ./tools/deploy-optimizer.sh [--live] +# +# Flags: +# --live Target mainnet/testnet (requires OPTIMIZER_PROXY and RPC_URL env vars). +# Without this flag the script targets a local Anvil instance. +# +# Environment (--live mode only): +# OPTIMIZER_PROXY Address of the deployed UUPS proxy to upgrade. +# RPC_URL JSON-RPC endpoint. +# SECRET_FILE Path to seed-phrase file (default: onchain/.secret). +# ============================================================================= + +set -euo pipefail + +# --------------------------------------------------------------------------- +# Paths +# --------------------------------------------------------------------------- +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)" +ONCHAIN_DIR="$REPO_ROOT/onchain" +TRANSPILER_DIR="$SCRIPT_DIR/push3-transpiler" +TRANSPILER_OUT="$ONCHAIN_DIR/src/OptimizerV3Push3.sol" + +# --------------------------------------------------------------------------- +# Parse arguments +# --------------------------------------------------------------------------- +LIVE=false +PUSH3_FILE="" + +for arg in "$@"; do + case "$arg" in + --live) LIVE=true ;; + --*) echo "Error: Unknown option: $arg" >&2; exit 1 ;; + *) PUSH3_FILE="$arg" ;; + esac +done + +if [ -z "$PUSH3_FILE" ]; then + echo "Usage: $0 [--live] " >&2 + exit 1 +fi + +if [ ! -f "$PUSH3_FILE" ]; then + echo "Error: File not found: $PUSH3_FILE" >&2 + exit 1 +fi + +# Make PUSH3_FILE absolute so it works regardless of cwd changes +PUSH3_FILE="$(cd "$(dirname "$PUSH3_FILE")" && pwd)/$(basename "$PUSH3_FILE")" + +# --------------------------------------------------------------------------- +# Helpers +# --------------------------------------------------------------------------- +info() { echo " [info] $*"; } +success() { echo " [ok] $*"; } +step() { echo; echo "==> $*"; } +fail() { echo; echo " [fail] $*" >&2; exit 1; } + +# Decode a uint256 returned by cast call (strips 0x prefix, converts hex→dec) +decode_uint() { + python3 -c "print(int('$1', 16))" 2>/dev/null || echo "0" +} + +# Decode a bool returned by cast call +decode_bool() { + python3 -c "print('true' if int('$1', 16) != 0 else 'false')" 2>/dev/null || echo "false" +} + +# Cleanup state +ANVIL_PID="" +SECRET_CREATED=false + +cleanup() { + if [ -n "$ANVIL_PID" ]; then + kill "$ANVIL_PID" 2>/dev/null || true + fi + if $SECRET_CREATED && [ -f "$ONCHAIN_DIR/.secret" ]; then + rm -f "$ONCHAIN_DIR/.secret" + fi + rm -f /tmp/deploy-local-output.txt /tmp/new-optimizer-impl.txt \ + /tmp/push3-test-addr.txt /tmp/upgrade-output.txt 2>/dev/null || true +} +trap cleanup EXIT + +# --------------------------------------------------------------------------- +# Step 0 — Validate tooling +# --------------------------------------------------------------------------- +step "Checking required tools" + +for tool in forge cast npx node python3; do + if ! command -v "$tool" &>/dev/null; then + fail "$tool not found in PATH" + fi +done +success "forge, cast, npx, node, python3 are present" + +# --------------------------------------------------------------------------- +# Step 1 — Transpile Push3 → Solidity +# --------------------------------------------------------------------------- +step "Transpiling $(basename "$PUSH3_FILE") → OptimizerV3Push3.sol" + +( + cd "$TRANSPILER_DIR" + if [ ! -d node_modules ]; then + info "Installing transpiler dependencies..." + npm install --silent + fi + npx ts-node src/index.ts "$PUSH3_FILE" "$TRANSPILER_OUT" +) + +success "Generated $TRANSPILER_OUT" + +# --------------------------------------------------------------------------- +# Step 2 — Compile with forge +# --------------------------------------------------------------------------- +step "Compiling contracts (forge build)" + +( + cd "$ONCHAIN_DIR" + forge build --silent +) + +success "Compilation succeeded" + +# --------------------------------------------------------------------------- +# Step 3 — Setup network target +# --------------------------------------------------------------------------- +step "Setting up network target" + +OPTIMIZER_PROXY="${OPTIMIZER_PROXY:-}" + +if $LIVE; then + # ---- Live / testnet mode ---- + info "Mode: LIVE (mainnet/testnet)" + + RPC_URL="${RPC_URL:-}" + if [ -z "$RPC_URL" ]; then + fail "--live requires RPC_URL env var" + fi + if [ -z "$OPTIMIZER_PROXY" ]; then + fail "--live requires OPTIMIZER_PROXY env var" + fi + + SECRET_FILE="${SECRET_FILE:-$ONCHAIN_DIR/.secret}" + if [ ! -f "$SECRET_FILE" ]; then + fail "Secret file not found: $SECRET_FILE (set SECRET_FILE env var)" + fi + + cast chain-id --rpc-url "$RPC_URL" >/dev/null 2>&1 || \ + fail "Cannot reach RPC endpoint: $RPC_URL" + + CHAIN_ID="$(cast chain-id --rpc-url "$RPC_URL")" + info "Connected to chain $CHAIN_ID via $RPC_URL" + info "Target proxy: $OPTIMIZER_PROXY" + info "Key file: $SECRET_FILE" +else + # ---- Dry-run (Anvil) mode ---- + info "Mode: DRY-RUN (local Anvil)" + RPC_URL="http://localhost:8545" + + # Ensure onchain/.secret exists for UpgradeOptimizer.sol (uses vm.readFile) + if [ ! -f "$ONCHAIN_DIR/.secret" ]; then + cp "$ONCHAIN_DIR/.secret.local" "$ONCHAIN_DIR/.secret" + SECRET_CREATED=true + info "Created temporary onchain/.secret from .secret.local" + fi + SECRET_FILE="$ONCHAIN_DIR/.secret" + + # Check if Anvil is already running + if cast chain-id --rpc-url "$RPC_URL" >/dev/null 2>&1; then + info "Anvil already running at $RPC_URL" + else + info "Starting Anvil..." + anvil --silent \ + --mnemonic "test test test test test test test test test test test junk" \ + --port 8545 & + ANVIL_PID=$! + # Poll until ready (no fixed sleeps) + TRIES=0 + until cast chain-id --rpc-url "$RPC_URL" >/dev/null 2>&1; do + TRIES=$((TRIES + 1)) + [ $TRIES -gt 50 ] && fail "Anvil did not start within 50 attempts" + sleep 0.2 + done + info "Anvil started (PID $ANVIL_PID)" + fi + + # If no OPTIMIZER_PROXY set, deploy a fresh local stack + if [ -z "$OPTIMIZER_PROXY" ]; then + info "No OPTIMIZER_PROXY set — deploying fresh local stack via DeployLocal.sol" + ( + cd "$ONCHAIN_DIR" + forge script script/DeployLocal.sol \ + --rpc-url "$RPC_URL" \ + --broadcast 2>&1 | tee /tmp/deploy-local-output.txt + ) + + CHAIN_ID="$(cast chain-id --rpc-url "$RPC_URL")" + BROADCAST_JSON="$ONCHAIN_DIR/broadcast/DeployLocal.sol/$CHAIN_ID/run-latest.json" + + if [ -f "$BROADCAST_JSON" ]; then + OPTIMIZER_PROXY="$(python3 - "$BROADCAST_JSON" <<'PYEOF' +import json, sys +path = sys.argv[1] +with open(path) as f: + data = json.load(f) +txs = data.get('transactions', []) +# The ERC1967Proxy is the optimizer proxy +for tx in txs: + name = (tx.get('contractName') or '').lower() + if 'erc1967proxy' in name: + print(tx.get('contractAddress', '')) + sys.exit(0) +# Fallback: look for "Optimizer:" in the deploy output +print('') +PYEOF +)" + fi + + # Fallback: grep the console log output + if [ -z "$OPTIMIZER_PROXY" ]; then + OPTIMIZER_PROXY="$(grep -oE 'Optimizer: 0x[0-9a-fA-F]{40}' \ + /tmp/deploy-local-output.txt | awk '{print $2}' | tail -1 || true)" + fi + + if [ -z "$OPTIMIZER_PROXY" ]; then + fail "Could not determine OPTIMIZER_PROXY from fresh deployment. Set OPTIMIZER_PROXY manually." + fi + info "Fresh stack deployed. Optimizer proxy: $OPTIMIZER_PROXY" + fi +fi + +# Derive private key for forge create / cast call operations +SEED="$(cat "$SECRET_FILE")" +DEPLOYER_KEY="$(cast wallet derive-private-key "$SEED" 0)" +DEPLOYER_ADDR="$(cast wallet address --private-key "$DEPLOYER_KEY")" +info "Deployer: $DEPLOYER_ADDR" + +# --------------------------------------------------------------------------- +# Step 4 — Capture pre-upgrade state +# --------------------------------------------------------------------------- +step "Capturing pre-upgrade optimizer parameters" + +# calculateSentiment(averageTaxRate, percentageStaked) is public pure — safe on both +# old and new implementations without needing a live Stake contract. +# +# Reference input: 95% staked (95e16), 5% tax rate (5e16) +REF_STAKED="950000000000000000" +REF_TAXRATE="50000000000000000" + +PRE_RAW="$(cast call "$OPTIMIZER_PROXY" \ + "calculateSentiment(uint256,uint256)(uint256)" \ + "$REF_TAXRATE" "$REF_STAKED" \ + --rpc-url "$RPC_URL" 2>/dev/null || echo "0x0")" +PRE_SENTIMENT="$(decode_uint "$PRE_RAW")" +info "Pre-upgrade calculateSentiment(staked=$REF_STAKED, tax=$REF_TAXRATE) = $PRE_SENTIMENT" + +# --------------------------------------------------------------------------- +# Step 5 — Deploy new implementation (for diff preview only) +# --------------------------------------------------------------------------- +step "Deploying new Optimizer implementation for diff preview" + +( + cd "$ONCHAIN_DIR" + forge create src/Optimizer.sol:Optimizer \ + --rpc-url "$RPC_URL" \ + --private-key "$DEPLOYER_KEY" \ + --json 2>/dev/null \ + | python3 -c "import json,sys; d=json.load(sys.stdin); print(d.get('deployedTo',''))" \ + > /tmp/new-optimizer-impl.txt +) + +NEW_IMPL="$(cat /tmp/new-optimizer-impl.txt 2>/dev/null || echo "")" +[ -z "$NEW_IMPL" ] && fail "Failed to deploy new Optimizer implementation" +info "New implementation deployed at: $NEW_IMPL" + +# calculateSentiment is pure — callable on bare (uninitialized) implementation +NEW_RAW="$(cast call "$NEW_IMPL" \ + "calculateSentiment(uint256,uint256)(uint256)" \ + "$REF_TAXRATE" "$REF_STAKED" \ + --rpc-url "$RPC_URL" 2>/dev/null || echo "0x0")" +NEW_SENTIMENT="$(decode_uint "$NEW_RAW")" +info "New impl calculateSentiment(staked=$REF_STAKED, tax=$REF_TAXRATE) = $NEW_SENTIMENT" + +# --------------------------------------------------------------------------- +# Step 6 — Show parameter diff before upgrading +# --------------------------------------------------------------------------- +step "Parameter diff (before upgrade confirmation)" + +echo +echo " ┌──────────────────────────────────────────────────────────────" +echo " │ calculateSentiment(averageTaxRate=$REF_TAXRATE, percentageStaked=$REF_STAKED)" +echo " ├──────────────────────────────────────────────────────────────" +printf " │ Old (via proxy) : %s\n" "$PRE_SENTIMENT" +printf " │ New (new impl) : %s\n" "$NEW_SENTIMENT" +if [ "$PRE_SENTIMENT" = "$NEW_SENTIMENT" ]; then + echo " │ Diff : none — implementations are semantically equivalent" +else + echo " │ Diff : CHANGED" +fi +echo " └──────────────────────────────────────────────────────────────" +echo + +if $LIVE; then + printf " Proceed with upgrade on LIVE network? [y/N] " + read -r CONFIRM + case "$CONFIRM" in + y|Y|yes|YES) ;; + *) echo "Upgrade cancelled."; exit 0 ;; + esac +fi + +# --------------------------------------------------------------------------- +# Step 7 — Run UUPS upgrade via UpgradeOptimizer.sol +# --------------------------------------------------------------------------- +step "Running UUPS upgrade (UpgradeOptimizer.sol)" + +( + cd "$ONCHAIN_DIR" + OPTIMIZER_PROXY="$OPTIMIZER_PROXY" \ + forge script script/UpgradeOptimizer.sol \ + --rpc-url "$RPC_URL" \ + --broadcast 2>&1 | tee /tmp/upgrade-output.txt +) || fail "UpgradeOptimizer.sol script failed" + +success "Proxy upgraded" + +# Confirm new implementation address from broadcast log +UPGRADED_IMPL="$(grep -oE 'New Optimizer implementation: 0x[0-9a-fA-F]{40}' \ + /tmp/upgrade-output.txt | awk '{print $NF}' | tail -1 || true)" +[ -n "$UPGRADED_IMPL" ] && info "Upgraded implementation: $UPGRADED_IMPL" + +# --------------------------------------------------------------------------- +# Step 8 — Round-trip verification +# --------------------------------------------------------------------------- +step "Round-trip verification (OptimizerV3Push3 isBullMarket)" + +# Deploy the transpiled OptimizerV3Push3 as a standalone test fixture. +# This contract has no constructor dependencies (pure functions only). +( + cd "$ONCHAIN_DIR" + forge create src/OptimizerV3Push3.sol:OptimizerV3Push3 \ + --rpc-url "$RPC_URL" \ + --private-key "$DEPLOYER_KEY" \ + --json 2>/dev/null \ + | python3 -c "import json,sys; d=json.load(sys.stdin); print(d.get('deployedTo',''))" \ + > /tmp/push3-test-addr.txt +) + +PUSH3_ADDR="$(cat /tmp/push3-test-addr.txt 2>/dev/null || echo "")" +[ -z "$PUSH3_ADDR" ] && fail "Failed to deploy OptimizerV3Push3 for round-trip verification" +info "OptimizerV3Push3 test fixture: $PUSH3_ADDR" + +# Test vectors derived from the Push3 program semantics: +# +# isBullMarket(percentageStaked, averageTaxRate) → +# if stakedPct ≤ 91% → false (always bear) +# else penalty = deltaS³ × effIdx / 20 +# if penalty < 50 → true (bull) +# else → false (bear) +# +# Vector 1 — Bear by staked threshold: +# 90% staked, any tax → stakedPct=90 ≤ 91 → false +# +# Vector 2 — Bear by penalty: +# 92% staked (deltaS=8), tax=1e17 → rawIdx=20, effIdx=21, +# penalty = 512×21/20 = 537 ≥ 50 → false +# +# Vector 3 — Bull: +# 99% staked (deltaS=1), tax=0 → rawIdx=0, effIdx=0, +# penalty = 1×0/20 = 0 < 50 → true + +PASS=true +VERIFY_LOG="" + +run_vector() { + local label="$1" pct="$2" tax="$3" expected="$4" + local raw actual + raw="$(cast call "$PUSH3_ADDR" \ + "isBullMarket(uint256,uint256)(bool)" \ + "$pct" "$tax" \ + --rpc-url "$RPC_URL" 2>/dev/null || echo "0x0")" + actual="$(decode_bool "$raw")" + if [ "$actual" = "$expected" ]; then + VERIFY_LOG="$VERIFY_LOG\n [PASS] $label" + else + VERIFY_LOG="$VERIFY_LOG\n [FAIL] $label (got=$actual expected=$expected)" + PASS=false + fi +} + +run_vector "Bear/threshold isBullMarket(90e16, 0) → false" \ + "900000000000000000" "0" "false" +run_vector "Bear/penalty isBullMarket(92e16, 1e17) → false" \ + "920000000000000000" "100000000000000000" "false" +run_vector "Bull/zero-tax isBullMarket(99e16, 0) → true" \ + "990000000000000000" "0" "true" + +echo +printf "%b\n" "$VERIFY_LOG" +echo + +if $PASS; then + success "Round-trip verification passed" +else + fail "Round-trip verification FAILED — transpiled isBullMarket does not match expected values" +fi + +# --------------------------------------------------------------------------- +# Summary +# --------------------------------------------------------------------------- +echo +echo "==> Pipeline complete" +echo " Push3 source : $(basename "$PUSH3_FILE")" +echo " Transpiled : $TRANSPILER_OUT" +echo " Proxy : $OPTIMIZER_PROXY" +echo " Network : $RPC_URL" +exit 0