harb/tools/deploy-optimizer.sh

456 lines
16 KiB
Bash
Raw Normal View History

#!/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