2026-03-10 20:21:54 +00:00
|
|
|
|
#!/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).
|
2026-03-20 08:47:28 +00:00
|
|
|
|
#
|
|
|
|
|
|
# Environment (dry-run mode only):
|
|
|
|
|
|
# ANVIL_FORK_URL Required when Anvil is not already running. Must point to
|
|
|
|
|
|
# a Base RPC endpoint so that forked state includes Uniswap V3
|
|
|
|
|
|
# Factory and WETH.
|
2026-03-10 20:21:54 +00:00
|
|
|
|
# =============================================================================
|
|
|
|
|
|
|
|
|
|
|
|
set -euo pipefail
|
|
|
|
|
|
|
2026-03-12 06:47:35 +00:00
|
|
|
|
# Foundry tools (forge, cast, anvil)
|
|
|
|
|
|
export PATH="${HOME}/.foundry/bin:${PATH}"
|
|
|
|
|
|
|
2026-03-10 20:21:54 +00:00
|
|
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
# 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
|
2026-03-19 21:23:38 +00:00
|
|
|
|
npx tsx src/index.ts "$PUSH3_FILE" "$TRANSPILER_OUT"
|
2026-03-10 20:21:54 +00:00
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
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"
|
|
|
|
|
|
|
2026-03-20 08:47:28 +00:00
|
|
|
|
# Check if Anvil is already running.
|
|
|
|
|
|
#
|
|
|
|
|
|
# DeployLocal.sol depends on live Base infrastructure: Uniswap V3 Factory at
|
|
|
|
|
|
# 0x4752ba5DBc23f44D87826276BF6Fd6b1C372aD24 and WETH at
|
|
|
|
|
|
# 0x4200000000000000000000000000000000000006. A plain (unfork'd) Anvil has
|
|
|
|
|
|
# neither, so cold-starting without --fork-url silently breaks the pipeline.
|
|
|
|
|
|
#
|
|
|
|
|
|
# When Anvil is already running (dev stack or CI), we use it as-is.
|
|
|
|
|
|
# When it is not running we require ANVIL_FORK_URL and start a forked instance.
|
2026-03-10 20:21:54 +00:00
|
|
|
|
if cast chain-id --rpc-url "$RPC_URL" >/dev/null 2>&1; then
|
|
|
|
|
|
info "Anvil already running at $RPC_URL"
|
|
|
|
|
|
else
|
2026-03-20 08:47:28 +00:00
|
|
|
|
ANVIL_FORK_URL="${ANVIL_FORK_URL:-}"
|
|
|
|
|
|
if [ -z "$ANVIL_FORK_URL" ]; then
|
|
|
|
|
|
fail "Anvil is not running at $RPC_URL and ANVIL_FORK_URL is not set.
|
|
|
|
|
|
DeployLocal.sol requires Base network contracts (Uniswap V3 Factory, WETH).
|
|
|
|
|
|
Either start a Base-forked Anvil externally, or set ANVIL_FORK_URL to a Base
|
|
|
|
|
|
RPC endpoint (e.g. ANVIL_FORK_URL=https://mainnet.base.org)."
|
|
|
|
|
|
fi
|
|
|
|
|
|
|
|
|
|
|
|
info "Starting Anvil (fork: $ANVIL_FORK_URL)..."
|
2026-03-10 20:21:54 +00:00
|
|
|
|
anvil --silent \
|
2026-03-20 08:47:28 +00:00
|
|
|
|
--fork-url "$ANVIL_FORK_URL" \
|
2026-03-10 20:21:54 +00:00
|
|
|
|
--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
|
2026-03-20 08:47:28 +00:00
|
|
|
|
info "Anvil started (PID $ANVIL_PID, fork: $ANVIL_FORK_URL)"
|
2026-03-10 20:21:54 +00:00
|
|
|
|
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"
|
2026-03-12 23:12:25 +00:00
|
|
|
|
forge script script/DeployLocal.sol --tc DeployLocal \
|
2026-03-10 20:21:54 +00:00
|
|
|
|
--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"
|
2026-03-12 21:15:35 +00:00
|
|
|
|
|
|
|
|
|
|
# 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
|
2026-03-10 20:21:54 +00:00
|
|
|
|
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"
|
2026-03-12 06:47:35 +00:00
|
|
|
|
forge create src/OptimizerV3.sol:OptimizerV3 \
|
2026-03-10 20:21:54 +00:00
|
|
|
|
--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 "")"
|
2026-03-12 06:47:35 +00:00
|
|
|
|
[ -z "$NEW_IMPL" ] && fail "Failed to deploy new OptimizerV3 implementation"
|
2026-03-10 20:21:54 +00:00
|
|
|
|
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
|