425 lines
15 KiB
Bash
Executable file
425 lines
15 KiB
Bash
Executable file
#!/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
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# 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 \
|
||
--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
|