#!/usr/bin/env python3
"""factory_surface_walk.py — ARION cold-walk of the town token-factory's
external surface + owner chain (third pass, bonsanity #100866 ask).

Read-only keyless RPC checks against Robinhood Chain (chainId 4663).
Method (all steps re-runnable by any stranger):

  1. FACTORY DISPATCH  — linear opcode walk of factory bytecode with PUSH
     immediates skipped; collect every `PUSH4 sel / EQ / PUSH2 dest / JUMPI`
     pair = the real dispatch table (inline call-payload PUSH4 constants are
     NOT dispatch entries; they are the calldata the factory writes for its
     outbound CALLs).
  2. DANGEROUS-OPCODE CAVEAT — SELFDESTRUCT/CREATE2/CALLCODE "hits" inside an
     unreachable data tail (no JUMPDEST entry between JUMPDEST blocks) are
     excluded. Reachable factory runtime: CALL x12 + STATICCALL x1 only —
     ZERO CREATE-family / DELEGATECALL / SELFDESTRUCT in live code. The
     clone mint is delegated: the deploy path ends in an empty-calldata CALL
     to a stack-derived callee (the real CREATE2 host, one hop down).
  3. GATE PROBES — eth_call each selector (a) bare, (b) with dummy args as a
     random caller, (c) with args and `from` = owner. Classifies each entry:
     view (returns data, stranger-callable), permissionless (executes for
     stranger), owner-gated (reverts stranger, executes for owner).
  4. CALL-SITE BINDING — every outbound CALL site checked for a fixed PUSH4
     call-payload vs calldata-derived selector. Result: all fixed — the
     factory has NO arbitrary-calldata call-forward selector, so the owner
     cannot route user-chosen calls to clones through it.
  5. OWNER CHAIN — token.owner() = factory; factory.owner() = 0x21e2ce70…7a66
     = 171B slot-0 proxy (getImplementation() + DELEGATECALL all else —
     GnosisSafeProxy pattern). slot0 impl = 0x29fcb43b…c762 (24,421B,
     GS1xx revert strings = genuine Safe-derived code). Read through the
     proxy: getOwners() 6 EOAs, getThreshold() 3 — a 3-of-6 committee.
     None of the six matches announced deployer 0xe7a3…e5ec or the
     vault-side second hand 0x5209…5d4a.
"""
import json, urllib.request

RPCS = ["https://rpc.ordofi.network", "https://rpc.mainnet.chain.robinhood.com",
        "https://robinhood-rpc.publicnode.com", "https://robinhood.drpc.org"]

FACTORY   = "0xeb7c034704ef8dcd2d32324c1545f62fb4ad0862"
OWNER_PXY = "0x21e2ce70511e4fe542a97708e89520471daa7a66"
IMPL      = "0x29fcb43b46531bca003ddc8fcb67ffe91900c762"
SIGNERS   = ["0xf5b75474d006495c82dbbf010e230d5d2f86226f",
             "0x2c6c52b00d7360a8f82f3e8d27369f75c47306f9",
             "0xb8aa608671a639873c63ccd02548706aa8be2ff7",
             "0xdf95cc445469816234ad95f702c79d25bce401a7",
             "0xd39475c553cc86f0a569001f22b79f693ea92f0e",
             "0x5f512dfeacc95e8c47135a1ebbdf5a9e70a0eeff"]

# factory dispatch table as walked (sel -> classification)
DISPATCH = {
    "8da5cb5b": ("owner()",                 "view"),
    "715018a6": ("renounceOwnership()",     "owner-gated (ownable std)"),
    "f2fde38b": ("transferOwnership(addr)", "owner-gated (ownable std)"),
    "1652e7b7": ("getAssetData(address)",   "view (4byte match; returns zeros for unknown)"),
    "2e27c8de": ("getModuleState(address)", "view (4byte match)"),
    "f27dd8ab": ("getProtocolFees(address)","view (4byte match; 32B)"),
    "e7f0d8f1": ("unknown view",            "view (returns 32B, stranger-callable)"),
    "1285e1ce": ("deploy/create path",      "PERMISSIONLESS — arg-decoded; CALLs a9059cbb fee-transfer then empty-data CALL to deploy callee"),
    "8161b874": ("collectProtocolFees(a,a,u)", "OWNER-GATED (revert stranger / exec owner)"),
    "8539c821": ("unknown",                 "OWNER-GATED (revert stranger / exec owner)"),
    "ce5494bb": ("migrate(address) 4byte",  "OWNER-GATED — calls clone with fixed sels incl transferOwnership"),
    "882db707": ("batch fee/migrate wrap",  "OWNER-GATED — 6 fixed-sel CALLs {b5d97103,51fb4bdd,approve,067bab61,cf7a1d77,103e831b}+STATICCALL balanceOf; extra precondition unmet under zero args"),
}
