Files
migration/v3v4/tools/test_red_oracle.py
zeekayandHanzo Dev 91af9c85b5 v3v4: harden mainnet V3->V4 migration executor (Red FIX-THEN-SHIP)
DEX LP migration toolkit for Lux C-Chain 96369. Contract (LiquidityDeployer.sol)
unchanged and RED-approved (16/16 tests). All hardening is in the Python executor,
plan builder, Makefile, and RUNBOOK.

FIX 1 (HIGH #4) oracle sanity bound — BLOCKED BROADCAST.
  The old one-sided guard (tick_o < v3edge-1000) only fired at ~25,000x error; a 10x
  oracle typo cloned a honeypot and let an LBTC-seller drain 4.29M WLUX. oracle.json is
  now a dual-control object per base {price, expected_tick(, band_ticks)}: the executor
  recomputes the tick from `price` and ABORTS if it strays > ORACLE_TICK_BAND (2000
  ticks ~= +/-22%) from the SECOND operator's independent expected_tick. A hard
  ONESIDED_MAX_WLUX cap (5,000,000e18) bounds per-pool WLUX at risk. The honeypot-edge
  check is kept as a backstop. Oracle validation runs BEFORE any V3 decrease.
  tools/execute.py: onesided_band_ok(), compute_onesided_bid(); tools/oracle.example.json.

FIX 2 (HIGH #5b) freeze baseline — prevents double-decrease to 75%.
  Re-running `make snapshot && make plan` mid-migration used to read already-halved
  liquidity as the new baseline and remove 50% of the remainder. build_plan.py now
  freezes tools/baseline.json (original liquidity + one-shot 50% target per tokenId) on
  first plan and reads it thereafter; a position already <= its baseline target is a
  no-op via the executor's on-chain precheck. Regenerating the plan is idempotent.

FIX 3 (MED #6) per-tx nonce + stop masking nonce errors.
  Re-read the live pending nonce before every tx and ABORT on drift (concurrent signer
  on treasury 0x9011); preflight asserts nonce stable across two reads 3s apart. A
  nonce too low/high is now FATAL (abort before the 240s receipt wait) instead of being
  masked as "sent"; benign "already known" still counts as accepted; 0/5 acceptance
  aborts.

FIX 4 (LOW) finalize + gas gate.
  Opt-in FINALIZE=1: after all adds, zero residual LD approvals and transferOwnership
  LD -> DAO Safe (0x51284...). Execute-mode preflight aborts if baseFee > gasPrice.
  state.json pools deduped (upsert by v4PoolId).

Verify: make test 16/16; tools/test_red_oracle.py PASS (K>=1.5x REJECT, 10x aborts);
make migrate dry-run clean end-to-end; fat-finger oracle aborts before touching V3.

Co-authored-by: Hanzo Dev <[email protected]>
2026-07-04 21:33:24 -07:00

93 lines
5.0 KiB
Python

#!/usr/bin/env python3
"""
RED repro for ATTACK #4: one-sided oracle fat-finger.
Replicates execute.py::phase_add_onesided tick/guard/ladder logic EXACTLY for the
LBTC/WLUX one-sided pool, then quantifies the WLUX an LBTC-seller can extract when
the operator mis-sets the oracle price K-times too high. Also finds the K at which
the honeypot guard (tick_o < v3edge - 1000) finally fires.
Read-only, pure math. No broadcast.
"""
import os, sys
HERE = os.path.dirname(os.path.abspath(__file__))
sys.path.insert(0, HERE)
from v4math import (get_sqrt_ratio_at_tick, get_tick_at_sqrt_price, sqrt_price_x96_from_price,
align_tick, liquidity_for_amount1, get_amounts_for_liquidity)
# The guard under test is imported from execute.py itself (single source of truth) so this
# repro proves the LIVE code path rejects a fat-finger, not a copy of it.
from execute import onesided_band_ok, ORACLE_TICK_BAND
SPACING = 60
BAND = 6900 # ONESIDED_BAND_TICKS default (bid ladder width)
V3_EDGE = 185729 # LBTC/WLUX v3 edge tick (plan.json)
WLUX_AMT = 4999999999999999999995806 # 50% of the LBTC bid = ~5,000,000 WLUX
LUX_USD = 12.5
FAIR_LBTC_USD = 63166.4 # oracle.json live value
# expected_tick = the operator's INDEPENDENT tick anchor, computed once from the fair mid.
EXPECTED_TICK = get_tick_at_sqrt_price(sqrt_price_x96_from_price(FAIR_LBTC_USD / LUX_USD))
def build_bid(asset_usd):
"""EXACT copy of execute.py one-sided branch for wlux_is_1 (currency1==WLUX)."""
price = asset_usd / LUX_USD # WLUX per LBTC (token1 per token0)
sqrtP = sqrt_price_x96_from_price(price)
tick_o = get_tick_at_sqrt_price(sqrtP)
guard_ok = tick_o < V3_EDGE - 1000
tu = align_tick(tick_o, SPACING, up=False)
tl = align_tick(tu - BAND, SPACING, up=False)
sL, sU = get_sqrt_ratio_at_tick(tl), get_sqrt_ratio_at_tick(tu)
L = liquidity_for_amount1(sL, sU, WLUX_AMT)
req = get_amounts_for_liquidity(sqrtP, tl, tu, L, True)[1]
while req > WLUX_AMT and L > 0:
L -= 1; req = get_amounts_for_liquidity(sqrtP, tl, tu, L, True)[1]
return dict(price=price, tick_o=tick_o, guard_ok=guard_ok, tl=tl, tu=tu, L=L, depositWLUX=req, sqrtP=sqrtP)
def wlux_out_when_true_price(bid, true_asset_usd):
"""How much WLUX an arber extracts by pushing the pool price down from tick_o to
the tick of the TRUE price (they sell LBTC while pool WLUX/LBTC > market).
WLUX released moving from tu down to max(tl, tick_true) = amount1 over that sub-range."""
true_price = true_asset_usd / LUX_USD
tick_true = get_tick_at_sqrt_price(sqrt_price_x96_from_price(true_price))
# arb profitable only while current pool tick > tick_true (pool pays > market)
stop = max(bid['tl'], min(bid['tu'], tick_true))
sStop = get_sqrt_ratio_at_tick(stop); sU = get_sqrt_ratio_at_tick(bid['tu'])
from v4math import get_amount1_delta, get_amount0_delta
wlux_out = get_amount1_delta(sStop, sU, bid['L'], False) # WLUX seller receives
lbtc_in = get_amount0_delta(sStop, sU, bid['L'], True) # LBTC seller delivers
lbtc_value_wlux = lbtc_in * true_price / 1e0 # fair WLUX value of that LBTC
return wlux_out, lbtc_in, lbtc_value_wlux
print("="*100)
print("LBTC/WLUX one-sided bid — oracle fat-finger analysis (FIX #1: tight expected_tick band)")
print("="*100)
fair = build_bid(FAIR_LBTC_USD)
fok, fwhy = onesided_band_ok(fair['tick_o'], EXPECTED_TICK)
print(f"FAIR oracle LBTC=${FAIR_LBTC_USD}: price={fair['price']:.2f} WLUX/LBTC tick_o={fair['tick_o']} "
f"expected_tick={EXPECTED_TICK} band=+/-{ORACLE_TICK_BAND} -> {'ACCEPT' if fok else 'REJECT'}")
print("\nFat-finger: operator types oracle K-times too high. NEW guard verdict (onesided_band_ok vs")
print("expected_tick), the OLD honeypot-only verdict, and the WLUX an arber drains at the true price:")
print(f"{'K':>7} {'oracleUSD':>12} {'tick_o':>9} {'dev':>8} {'NEWguard':>9} {'OLDhoneypot':>12} {'WLUXdrained':>15} {'lossWLUX':>15}")
rows = []
for K in [1, 1.5, 2, 3, 5, 10, 50, 100, 1000]:
b = build_bid(FAIR_LBTC_USD * K)
if b['L'] <= 0:
print(f"{K:>7} L<=0"); continue
new_ok, _ = onesided_band_ok(b['tick_o'], EXPECTED_TICK)
dev = abs(b['tick_o'] - EXPECTED_TICK)
wlux_out, lbtc_in, fair_val = wlux_out_when_true_price(b, FAIR_LBTC_USD)
loss = wlux_out - fair_val
rows.append((K, new_ok))
print(f"{K:>7} {FAIR_LBTC_USD*K:>12,.0f} {b['tick_o']:>9} {dev:>8} "
f"{('ACCEPT' if new_ok else 'REJECT'):>9} {('ACCEPT' if b['guard_ok'] else 'REJECT'):>12} "
f"{wlux_out/1e18:>15,.2f} {loss/1e18:>15,.2f}")
# assert the fix: K==1 accepted, every K>=1.5 rejected by the NEW guard.
bad = [K for K, ok in rows if (K == 1 and not ok) or (K >= 1.5 and ok)]
print("\nFIX #1 verification:")
if bad:
print(f" FAIL — guard misbehaved at K={bad}")
sys.exit(1)
print(f" PASS — K=1 ACCEPT; every K>=1.5 REJECT (band +/-{ORACLE_TICK_BAND} ticks). "
f"A 10x typo (dev~{abs(build_bid(FAIR_LBTC_USD*10)['tick_o']-EXPECTED_TICK)} ticks) aborts immediately.")