---
name: rugscan-4
description: Continuation of [[rugscan-3]] from 13 Sep 2026. The X false-SCAM post mortem, four more detector bugs found in the audit that followed, the RPC fast path that cut a scan from 104s to ~28s, and selftest.py - the gate that must pass before any scanner change ships. Read with [[rugscan]], [[rugscan-2]] and [[rugscan-3]].
sources: [cowork]
aliases: [rugscan part 4, rug-scan-4, selftest, sell-tax-bug, fast-rpc, proxy-blindness]
---

# rugscan, part 4 (13 Sep 2026)

Same box: **/opt/rugscan** on kookwatch (159.223.88.168), key `~/kookwatch/id_kookwatch` on the Mac.

## THE TRIGGER
- [stated] Ben pasted the rugwatch alert "⛔⛔ YOU JUST BOUGHT A SCAM ⛔⛔ X - SCAM risk 100/100 ... sells are taxed 20% in practice" on X `0x9e02541f3dace0e9e736141ee01526b46bb7a8d3`, and asked what it was about and why he was not getting the right number.
- [stated] Then: "Fix the alert thing... Make sure this never happens again. Any other mistakes you can find in the bot... Double-check everything... Also, it needs to be much faster."

## THE X FALSE POSITIVE, root cause
The alert was wrong. Measured on chain: **buy costs 1% at the front end plus 5.0% skimmed in tokens to the Pons fee wallet `0xe5e702641ea86f4ae6cc3cdaed2b886f976be044`; sell costs 5.0 to 5.95% on the ETH leg. Round trip about 12%.** 326 of 326 sells in the window settled normally. Receipt: a 0.1 ETH buy, tx `0x6f94ab07046a366888bf30e8bbd645370002706f3173694822f98120252b6362`, paid out 1,838,363 X of which exactly 91,918 (5.0%) went to the fee wallet.
- `detect._exit_window` computed `median_sell_tax_pct` from **n=2 transactions** out of 3,328 transfers: one read 0.00%, one read 39.09%, median 19.55.
- The 39.09% tx `0xb72248d148add8a28acf2974cd4e1d85d1605c4626bb1b99a1446f5f28273cf3` was a batched relayer sell: A -> R1 -> PoolManager and B -> R1 -> R2 -> PoolManager. `tot` summed every outbound leg of every debited address, so the same 227,871 tokens were counted twice. **A repeat of the 5 Sep "gross vs net" bug class in a new place.**
- `low_confidence` was False because it watches the BUYER count (325). **The confidence gate was measuring a different sample from the number it was gating.** That is the generalisable lesson: a confidence flag must be computed from the same n as the statistic it guards.
- My own first answer to Ben quoted the project's docs (4% each way, 8% round trip) instead of measuring. Corrected in-thread. Rule reinforced: read the chain, never the docs.

## THE OTHER FOUR BUGS THE AUDIT FOUND
1. **PROXY BLINDNESS, the worst of them.** CATGPT, TITCOIN, QUANTUMCOIN, INVEST and HENT all have **44 bytes** of runtime: `3d3d3d3d363d3d37363d73<impl>5af43d3d93803e602a57fd5bf3`, a Solady/0age minimal proxy, all pointing at implementation `0x3be8b97fd0e713b5abe0649fa830223b6b4bc599`. `contract_battery` read the 44-byte stub, found no selectors and no strings, and returned **sev 0 = clean**. It was not clean, it was blind. Meanwhile `owner()` delegated through to the factory `0xeb7c034704ef8dcd2d32324c1545f62fb4ad0862` and `contract_risk` charged +15 and printed "owner is NOT renounced" on **every token that pad has ever launched** - it drove five AVOID calls in two days. Fixed with `detect.implementation()` / `detect.logic_code()`: match `73<40 hex>5af43d` (covers EIP-1167 and the 44-byte variant) plus the EIP-1967 slot, then analyse the IMPLEMENTATION. Under 200 bytes with no resolvable target now fails CLOSED.
2. **`transferOwnership()` was severity 2**, and "owner not renounced + sev>=2" escalates to sev 3 = hard SCAM. Once proxies became readable every plain Ownable clone would have been called a scam. Demoted to severity 1: it is a power over the other powers, and with no other powers it can do nothing to a holder. `contract_risk` now only charges for an owner when the code actually has switches.
3. **`fresh_wallets` had been returning `checked: 0` on EVERY scan** while costing 13.6s of retries. Measured cause: Blockscout v1 `action=tokentx` **429**, v1 `txlist` **429**, v2 `/addresses/{a}` **500**. Rewritten twice: nonce-at-launch-block failed because **the public RH RPC is not archive** ("metadata is not found"); the working version compares the wallet's lifetime nonce against the number of txs it has with this token in the window, and reports unmeasurable past 36h rather than guessing.
4. **`chain.code_at` still returned `"0x"` on failure** - the exact fail-open bug [[rugscan]] records as fixed on 6 Sep, alive again in chain.py and in `cluster_live`. Now returns **None**, `is_contract` returns True/False/**None**, and `cluster_live` has a third `unknown_pct` bucket that is reported and never scored.
5. **Thin liquidity was scored as risk** (`liq < 8000` -> +8 and a headline reason; it drove GPU to AVOID 60). Ben's standing rule is that a coin is never downranked for being small or thin. Moved to context.

## THE SPEED WORK: 103.9s -> 26 to 33s
Profiled first, guessed nothing. Serial baseline: exit_check 18.9s, cluster_live 16.3s, fresh_wallets 13.6s (for checked=0), relayers 10.2s, battery 10.0s, impersonation 9.7s, transfers 8.1s, contract_risk 5.3s - and almost none of it depended on anything else.
- **THE BIG ONE: the official RPC is slow and the alternatives are not.** One eth_call, same data: `rpc.mainnet.chain.robinhood.com` **1.75s**, `robinhood-rpc.publicnode.com` **0.11s and serves a 20-call JSON-RPC batch in 0.12s**, `robinhood.drpc.org` **0.04s and IS AN ARCHIVE NODE** (historical nonce works there). **Only the official node serves eth_getLogs** (drpc 400, publicnode 403), and it 429s any batch. So: `chain.fast()` / `fast_batch()` / `balances()` / `codes()` for every point lookup, `rpc()` for logs only. 60 balanceOf reads went from ~16s to 0.37s. publicnode also 403s any pinned (non-"latest") block; drpc serves them.
- **Never read a second node without proving it is the same chain at the same tip.** `warm_rpc()` checks chainId and head (tolerance 600 blocks), caches to `.fastrpc.json` for 6h. It MUST run before any worker starts: resolving it lazily meant the first thread held the lock while probing the slow node, and the profile showed total_supply, decimals and code_at each taking 15.4-15.7s for calls that take 0.3s. They were not slow, they were waiting.
- **Parallel eth_getLogs is not available on this chain, at any width.** Measured over 24,000 blocks: 1 worker 12.74s and 0 errors; 3 workers 4.29s and 4x HTTP 429; 6 workers 3.08s and 5x 429, losing two thirds of the logs. Reverted to serial.
- **A 429 must never cause a bisect.** `_logs` treated any exception as "range too big" and split, which doubles the requests against a node already refusing - a rate-limit death spiral, same shape as the 11,000-call incident. New `RpcBusy` exception: back off and retry the SAME range, never split.
- **One log reader at a time, process wide** (`chain.LOG_GATE`). transfers and exit_check were 429ing each other and spending the difference in backoff; taking turns measured faster.
- **EXIT_LADDER 2,12,48,168 hours -> 0.25,1,4,24,168.** Two hours on a 0.1s chain is ~71,000 blocks. The check only needs 40 buyers and a busy token reaches 300+ in fifteen minutes.
- Remaining floor is the log pull on the one node that serves it: ~12s launch window plus ~5s escape window. The selftest budget is set to 35s, which is measured and honest, not aspirational.

## THE GATE: /opt/rugscan/selftest.py
**Run `cd /opt/rugscan && python3 selftest.py` before trusting ANY change to the scanner.** 26 checks in 8 cases, every one of them a thing that already went wrong in production: fast-path parity against the official node at a PINNED block, the exact X relayer-hop arithmetic, the thin-sample gate replayed through the real `judge()`, proxy resolution, fail-closed on unknown, liquidity never scored, freshness measuring more than zero, and a whole scan inside the time budget. Cases are added, never deleted.
- **Compare pinned, never two "latest" reads.** On a token trading every block the official node disagreed with ITSELF on 2 of 10 addresses read seconds apart. Pinned to one block, official and fast agreed 10 of 10. The first version of this test failed for that reason and the test was wrong, not the code.

## THE OPERATIONAL TRAP THAT ALMOST HID ALL OF IT
**rugwatch.service is a long-running daemon: it holds the OLD detect.py in memory until restarted.** ORIONIS still fired "SCAM, sells taxed 32%" at 15:39, nine minutes after the fix landed on disk, because the service had been up since 12 Sep 06:15. **Patching /opt/rugscan is not deploying it.** `systemctl restart rugwatch.service kookwatch.service` is part of every change; launchwatch is cron and picks it up on its own.

## FINAL STATE, verified 13 Sep 2026 17:00 UTC
`./deploy.sh` green: **29 checks passed, 0 failed**, daemons restarted, exit 0.
Re-scan of every wrong call: **X SCAM 100 -> CAUTION 52 · ICAT SCAM 100 -> CLEAN 10 · ORIONIS SCAM 100 -> CAUTION 52 · PONSUM AVOID 72 -> CAUTION 52 · CATGPT AVOID 67 -> CLEAN 0 · INVEST AVOID 67 -> CLEAN 0 · GPU AVOID 60 -> AVOID 67 (now for the right reason: 7 buyers in four hours, a dead market, not a thin pool)**. Controls held: DELTA CLEAN 0, PAIR CLEAN 0.
Timing: live tokens **16 to 24s** (was 103.9s), dead ones ~27 to 91s because the escape ladder still has to widen to find buyers. Two more knobs did most of the last of it: **LOG_CHUNK 3000 -> 10000** (the cost is round trips, not answer size, and the RpcLimit bisect covers the rare overflow) and **stop widening the ladder once four hours still shows fewer than EXIT_THIN buyers**.
Trap hit again while doing this, third time in the file: **`pkill -f reg3.py` killed the calling shell**, because the shell's own command line contains the pattern. Always `for p in $(pgrep -f "^python3 /tmp/x"); do kill $p; done`.

## ALERTS THAT WERE WRONG, for the record
Three SCAM calls in 48h, all from the sell-tax bug: **ICAT `0xbe4a5bc82d8b6569a0a646cd5d4365c32d59cc89` "16%", X "20%", ORIONIS `0xfceae4df6961cbb3d22491b72e66c7adf415bca8` "32%"**. Two more carried the softer "sells lose X% to tax" line: ZenoCoin 15%, CATGPT 9%. Five AVOIDs were driven by the proxy-owner false positive, one (GPU) by the thin-pool rule, and PONSUM AVOID 72 rested on "only 14.7% of buyers have managed to sell", which was the relayer undercount.

## Patch files on the droplet, in order
`patch_tax.py` (net-not-gross + TAX_MIN_N=8), `patch_correct.py` (fail-closed code_at, relayer passthrough, fresh rewrite, liquidity to context), `patch_speed2.py` (shared eth_getCode, phase-0 thread pool), `patch_proxy.py` (implementation resolution), `patch_sev.py` (transferOwnership severity), `patch_fastrpc.py` (fast/fast_batch/balances/codes), `patch_parlogs.py`, `patch_ratelimit.py` (reverts parallel logs, RpcBusy), `patch_warm.py` (warm_rpc), `patch_loggate.py`. Every one uses exact-string replacement with asserts, `ast.parse` before write, and timestamped `.bak-` backups.
