Merge pull request 'fix: Unified Push3 → deploy pipeline: transpile, compile, upgrade in one command (#538)' (#547) from fix/issue-538 into master
This commit is contained in:
commit
08b9a3df30
1 changed files with 425 additions and 0 deletions
425
tools/deploy-optimizer.sh
Executable file
425
tools/deploy-optimizer.sh
Executable file
|
|
@ -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] <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
|
||||
Loading…
Add table
Add a link
Reference in a new issue