harb/tools/deploy-optimizer.sh
openhands 73a80ead0b fix: add --tc DeployLocal to forge script invocations
Adding SeedSwapper alongside DeployLocal in the same .sol file caused
forge to error "Multiple contracts in the target path" when no --tc flag
was specified, silently failing the CI bootstrap step.

Add --tc DeployLocal to all forge script invocations of DeployLocal.sol:
  - scripts/bootstrap-common.sh  (CI / local bootstrap)
  - tools/deploy-optimizer.sh    (manual deploy tool)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-12 23:12:25 +00:00

455 lines
16 KiB
Bash
Executable file
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/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] <input.push3>
#
# 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
# Foundry tools (forge, cast, anvil)
export PATH="${HOME}/.foundry/bin:${PATH}"
# ---------------------------------------------------------------------------
# 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] <input.push3>" >&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 --tc DeployLocal \
--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"
# Verify that the seed trade bootstrapped VWAP during deployment.
# DeployLocal.sol runs a first recenter + seed buy + second recenter so that
# cumulativeVolume>0 before any user can interact with the protocol.
LM_ADDR=""
if [ -f "$BROADCAST_JSON" ]; then
LM_ADDR="$(python3 - "$BROADCAST_JSON" <<'PYEOF'
import json, sys
with open(sys.argv[1]) as f:
data = json.load(f)
for tx in data.get('transactions', []):
if (tx.get('contractName') or '').lower() == 'liquiditymanager':
print(tx.get('contractAddress', ''))
break
PYEOF
)"
fi
if [ -n "$LM_ADDR" ]; then
CUMVOL_HEX="$(cast call "$LM_ADDR" "cumulativeVolume()(uint256)" \
--rpc-url "$RPC_URL" 2>/dev/null || echo "0x0")"
CUMVOL="$(decode_uint "$CUMVOL_HEX")"
if [ "$CUMVOL" -gt 0 ]; then
success "VWAP bootstrapped: LiquidityManager.cumulativeVolume=$CUMVOL"
else
fail "VWAP not bootstrapped: cumulativeVolume=0 — seed trade may have failed"
fi
fi
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/OptimizerV3.sol:OptimizerV3 \
--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 OptimizerV3 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