Files
zeekay db5d60fe08 docs(migration): ownership consolidation is SOLVED — runbook, manifest, proof
SWEEP-RUNBOOK previously recorded ownership as BLOCKED: the 0xce15 key was
unlocated, so the step emitted calldata and was a no-op. That is no longer the
state, and the runbook said otherwise.

Adds OWNERSHIP-RUNBOOK.md with two paths that together move the ENTIRE mainnet
(96369) DEX to the DAO Safe:
  Path A — a StateUpgrade re-owns the 23 contracts whose keys are lost or
           unknown (0xce15, 0xbe7A89, 0x086F4aA1), fork-proven 109/109
  Path B — direct transfer of the 74 LP NFTs + 7 ERC-20 balances from 0x9011

Ships the artifacts that back the claim rather than the claim alone:
manifests/96369-ownership.stateupgrade.json (the storage writes),
proof/ownership-fork-e2e-PASS.log (the fork run), and the program/ scripts for
both paths.

The manifest holds contract STORAGE SLOT writes — 64-hex slot ids mapping to
owner addresses, with blockTimestamp left as __OWNERSHIP_MIGRATION_TS__.
Checked explicitly, because slot ids and private keys are indistinguishable to
a naive scan: there are no keys here. The runbook references k8s secret names
and KMS paths only.
2026-07-26 08:53:15 -07:00

80 lines
4.1 KiB
Bash
Executable File

#!/usr/bin/env bash
# Path-B ownership consolidation: transfer ALL of 0x9011's V3 LP position NFTs
# (+ optional extra tokenIds) to the DAO Safe. User-authorized ("transfer all, move all").
# Reuses the proven sweep harness: lux-deployer/LUX_PRIVATE_KEY signs (0x9011, never printed),
# keepwarm.sh (idx-777) keeps the demand-driven builder hot so txs land.
#
# Uses transferFrom (NOT safeTransferFrom) so the receive never depends on the Safe's
# onERC721Received fallback handler — the Safe custodies the NFT unconditionally, and can
# later move it via execTransaction. Enumerates ALL tokenIds FIRST (indices shift as we
# transfer), then transfers by tokenId, then verifies every ownerOf == DAO Safe.
#
# Env: RPC (required, a PINNED caught-up node), SECRET_NS (default lux-mainnet),
# PM (V3 position manager), DAO (target owner), EXECUTE=yes to actually send.
set -uo pipefail
CTX=do-sfo3-lux-k8s
SECRET_NS="${SECRET_NS:-lux-mainnet}"
OWNER=0x9011E888251AB053B7bD1cdB598Db4f9DEd94714
PM="${PM:-0x7a4C48B9dae0b7c396569b34042fcA604150Ee28}"
DAO="${DAO:-0x51284dC2133e8d3a8e213DCa6a6FA768cfDfcce2}"
: "${RPC:?set RPC to a pinned caught-up mainnet node}"
cid=$(cast chain-id --rpc-url "$RPC" 2>/dev/null)
[ "$cid" = "96369" ] || { echo "REFUSE: chainId=$cid != 96369 (wrong net / not pinned)"; exit 1; }
read_key() { kubectl --context $CTX get secret lux-deployer -n "$SECRET_NS" -o jsonpath='{.data.LUX_PRIVATE_KEY}' | base64 -d; }
MK=$(read_key); case "$MK" in 0x*) ;; *) MK="0x$MK";; esac
A=$(cast wallet address --private-key "$MK" 2>/dev/null)
[ "$(echo "$A" | tr A-Z a-z)" = "$(echo "$OWNER" | tr A-Z a-z)" ] || { echo "REFUSE: signer $A != 0x9011"; MK=; exit 1; }
echo "[lp] signer verified = 0x9011 chainId=$cid head=$(cast block-number --rpc-url "$RPC")"
# --- enumerate ALL tokenIds FIRST (before any transfer; indices shift as balance drops) ---
n=$(cast call "$PM" 'balanceOf(address)(uint256)' "$OWNER" --rpc-url "$RPC" | sed 's/ .*//')
echo "[lp] 0x9011 holds $n LP position NFTs — enumerating (with retry)..."
IDS=()
for i in $(seq 0 $((n-1))); do
tid=""; for try in 1 2 3 4 5; do
tid=$(cast call "$PM" 'tokenOfOwnerByIndex(address,uint256)(uint256)' "$OWNER" "$i" --rpc-url "$RPC" 2>/dev/null | sed 's/ .*//')
[ -n "$tid" ] && break || sleep 1
done
[ -n "$tid" ] && IDS+=("$tid") || { echo "REFUSE: idx $i unreadable after retries"; MK=; exit 1; }
done
echo "[lp] enumerated ${#IDS[@]} tokenIds: ${IDS[*]}"
[ "${#IDS[@]}" -eq "$n" ] || { echo "REFUSE: enumerated ${#IDS[@]} != balance $n"; MK=; exit 1; }
if [ "${EXECUTE:-no}" != "yes" ]; then
echo "[lp] PLAN ONLY (set EXECUTE=yes to send). Would transferFrom each of the ${#IDS[@]} tokenIds 0x9011 -> $DAO"
MK=; exit 0
fi
# --- keep the builder hot (idx-777, prefunded from 0x9011) so each transfer lands fast ---
echo "[lp] pre-warming builder..."
bash "$(dirname "$0")/keepwarm.sh" "$RPC" prefund 2>&1 | grep -viE "fs_permissions|nightly|Warning" | tail -1
bash "$(dirname "$0")/keepwarm.sh" "$RPC" run 3 >/tmp/lp-keepwarm.log 2>&1 &
KW=$!
sleep 6
# --- transfer each (sequential, blocks until mined; nonce auto-increments on 0x9011) ---
ok=0; fail=0
for tid in "${IDS[@]}"; do
if cast send "$PM" 'transferFrom(address,address,uint256)' "$OWNER" "$DAO" "$tid" \
--private-key "$MK" --rpc-url "$RPC" --gas-limit 250000 >/tmp/lp-tx-$tid.log 2>&1; then
now=$(cast call "$PM" 'ownerOf(uint256)(address)' "$tid" --rpc-url "$RPC" | head -c 42)
if [ "$(echo "$now" | tr A-Z a-z)" = "$(echo "$DAO" | tr A-Z a-z)" ]; then
ok=$((ok+1)); printf '[lp] tokenId %-8s -> DAO ✓ (%d/%d)\n' "$tid" "$ok" "${#IDS[@]}"
else
fail=$((fail+1)); printf '[lp] tokenId %-8s SENT but owner=%s ✗\n' "$tid" "$now"
fi
else
fail=$((fail+1)); printf '[lp] tokenId %-8s SEND FAILED (see /tmp/lp-tx-%s.log)\n' "$tid" "$tid"
fi
done
kill $KW 2>/dev/null
# --- final verify ---
rem=$(cast call "$PM" 'balanceOf(address)(uint256)' "$OWNER" --rpc-url "$RPC" | sed 's/ .*//')
daon=$(cast call "$PM" 'balanceOf(address)(uint256)' "$DAO" --rpc-url "$RPC" | sed 's/ .*//')
echo "[lp] === DONE: transferred=$ok failed=$fail ; 0x9011 now holds $rem LP NFTs ; DAO Safe holds $daon ==="
MK=
[ "$fail" -eq 0 ] && [ "$rem" = "0" ]