# Research record

This directory contains the reproducible evidence behind `../post.md`. The chain snapshot was
read on 2026-09-04 from the public Robinhood Chain RPC: [rpc.mainnet.chain.robinhood.com](https://rpc.mainnet.chain.robinhood.com).
The scanner sends one JSON-RPC request per call; it never uses JSON-RPC batching.

Multi-chain census config lives in `chains.json`. Robinhood results stay in `data/`; other
chains write to `data/<name>/`. Proven end-to-end on unichain (`data/unichain/`).

One-line census + probe per chain (from repo root, with `PYTHONPATH=topics/bait-hooks-and-intents/research`):

```bash
python3 topics/bait-hooks-and-intents/research/scan_rpc.py --chain ethereum --phase init && python3 topics/bait-hooks-and-intents/research/probe_hooks.py --chain ethereum --all
python3 topics/bait-hooks-and-intents/research/scan_rpc.py --chain base --phase init && python3 topics/bait-hooks-and-intents/research/probe_hooks.py --chain base --all
python3 topics/bait-hooks-and-intents/research/scan_rpc.py --chain arbitrum --phase init && python3 topics/bait-hooks-and-intents/research/probe_hooks.py --chain arbitrum --all
python3 topics/bait-hooks-and-intents/research/scan_rpc.py --chain unichain --phase init && python3 topics/bait-hooks-and-intents/research/probe_hooks.py --chain unichain --all
python3 topics/bait-hooks-and-intents/research/scan_rpc.py --chain optimism --phase init && python3 topics/bait-hooks-and-intents/research/probe_hooks.py --chain optimism --all
python3 topics/bait-hooks-and-intents/research/scan_rpc.py --chain polygon --phase init && python3 topics/bait-hooks-and-intents/research/probe_hooks.py --chain polygon --all
python3 topics/bait-hooks-and-intents/research/scan_rpc.py --chain bnb --phase init && python3 topics/bait-hooks-and-intents/research/probe_hooks.py --chain bnb --all
python3 topics/bait-hooks-and-intents/research/scan_rpc.py --chain avalanche --phase init && python3 topics/bait-hooks-and-intents/research/probe_hooks.py --chain avalanche --all
python3 topics/bait-hooks-and-intents/research/scan_rpc.py --chain robinhood --phase init && python3 topics/bait-hooks-and-intents/research/probe_hooks.py --chain robinhood --all
```

## Reproduction commands

Run from the repository root:

```bash
python3 -m venv .venv
.venv/bin/python -m pip install -r topics/bait-hooks-and-intents/requirements.txt

.venv/bin/python topics/bait-hooks-and-intents/research/scan_rpc.py \
  --phase init \
  --out-dir topics/bait-hooks-and-intents/research/data

PYTHONPATH=topics/bait-hooks-and-intents/research \
.venv/bin/python topics/bait-hooks-and-intents/research/probe_hooks.py \
  --pool-csv topics/bait-hooks-and-intents/research/data/v4_dynamic_before_swap_pools.csv \
  --out-csv topics/bait-hooks-and-intents/research/data/incident_hook_direct_probes.csv \
  --hooks 0x0d83bebf6544de34a591dd5fc96a85500c72e880 \
          0x62d45c9b299c46b07b3905a9221926fe7cf34080 \
          0xd0a7c0df77eed9595f01813bd5cfdaa427250080 \
          0x349cb68185bb6243242adde5ed178c97f27180c4

.venv/bin/python topics/bait-hooks-and-intents/research/scan_rpc.py \
  --phase swaps \
  --hooks 0x0d83bebf6544de34a591dd5fc96a85500c72e880 \
          0x62d45c9b299c46b07b3905a9221926fe7cf34080 \
          0xd0a7c0df77eed9595f01813bd5cfdaa427250080 \
          0x349cb68185bb6243242adde5ed178c97f27180c4 \
  --out-dir topics/bait-hooks-and-intents/research/data

PYTHONPATH=topics/bait-hooks-and-intents/research \
.venv/bin/python topics/bait-hooks-and-intents/research/fee_flow.py \
  --from-block 0 \
  --to-block 54223819

.venv/bin/python topics/bait-hooks-and-intents/research/derive_daily.py
PYTHONPATH=topics/bait-hooks-and-intents \
.venv/bin/python topics/bait-hooks-and-intents/render_charts.py
```

The first command scans `Initialize(bytes32,address,address,uint24,int24,address,uint160,int24)`
from block 0 to the observed latest block, then keeps `fee == 0x800000` and the `BEFORE_SWAP`
permission bit (`0x80`) in the hook address. The Swap phase scans each selected pool from its own
Initialize block through the observed latest block. The event topics are recorded in
`scan_rpc.py` and can be independently checked against the [Uniswap v4 PoolManager source](https://github.com/Uniswap/v4-core/blob/main/src/PoolManager.sol).

`fee_flow.py` accepts `--from-block`, `--to-block`, and a phase selector. Liquidity rows are merged
by transaction hash and log index, and each completed pool is written immediately, so an interrupted
range can be resumed without duplicating committed events. It uses the same adaptive range bisection
as `scan_rpc.py` while preserving the endpoint's one-request-per-call constraint.

`incident_hook_direct_probes.csv` uses a valid PoolKey, a one-unit probe, `from = PoolManager`,
and two calls at the same exact latest block: omitted `gasPrice`, then `gasPrice = baseFeePerGas`.
The return’s third word is decoded as `lpFeeOverride`; low 22 bits are the fee and bit `0x400000`
is the override flag. The direct probe is a hook-context test. The “real tx” values in the post’s
fee chart come only from the verified filler trace snapshot copied into `data/filler_evidence.csv`.

The USDG-side volume estimate uses `abs(Swap amount on the USDG side) / 10^6`. The six-decimal
denomination is an on-chain `eth_call` to the public USDG token’s `decimals()` selector. Estimated
fee extraction is `USDG-side volume × Swap.fee / 1,000,000`; `Swap.fee` is the event’s actual fee
field. Daily buckets are the UTC calendar day of each swap block's timestamp, fetched per block by
`block_times.py` into `block_timestamps.csv` (this chain produces roughly 850k blocks a day, so
block-count day estimates are not usable).

## Fee-flow interpretation

The relevant V4 interfaces are:

```text
beforeSwap(address sender, PoolKey calldata key, IPoolManager.SwapParams calldata params, bytes calldata hookData)
  returns (bytes4, BeforeSwapDelta, uint24 lpFeeOverride)
modifyLiquidity(PoolKey memory key, ModifyLiquidityParams memory params, bytes calldata hookData)
  returns (BalanceDelta callerDelta, BalanceDelta feesAccrued)
take(Currency currency, address to, uint256 amount)
```

PoolManager passes `lpFeeOverride` into the pool swap, where the input-side LP fee increments
`feeGrowthGlobal`. The three incident hooks return a zero `BeforeSwapDelta` in the committed probes,
lack the applicable return-delta permission bits, and have no scanned hook-operated ERC-6909 claim
events. The exact behavior is independently readable in the official [IHooks interface](https://github.com/Uniswap/v4-core/blob/main/src/interfaces/IHooks.sol),
[PoolManager implementation](https://github.com/Uniswap/v4-core/blob/main/src/PoolManager.sol), and
[Pool fee accounting](https://github.com/Uniswap/v4-core/blob/main/src/libraries/Pool.sol).

The public endpoint exposes logs, receipts, and current contract state, but not historical state for
these blocks or a callable transaction-trace method. Consequently, `fee_flow_mechanism.csv` records
current non-zero fee-growth words rather than claiming historical per-swap deltas, and native ETH
collection rows are marked `not established`. ERC-20 USDG transfers are exact, but every observed
PoolManager outflow coincides with a non-zero liquidity removal, so principal and fees cannot be
separated from the receipt alone. Two-hop rows are address-level observations; fungible provenance
and bridge or exchange ownership are not inferred.

## Evidence map

| Post claim | Evidence in this directory |
|---|---|
| Pool and hook census | `data/v4_dynamic_before_swap_pools.csv`, `data/scan_meta.json` |
| Two-context classifications | `data/incident_hook_direct_probes.csv` |
| Swap count, callers, volume, extracted-fee estimate | `data/swap_events.csv`, `data/bait_pool_stats.csv`, `data/daily_bait_stats.csv` |
| Six-fill replay and filler gas facts | `data/filler_evidence.csv` |
| Verified fee-at-zero vs real-tx chart | `data/fee_by_hook.csv` |
| User cost model | `data/cost_comparison.csv` |
| Hook return, permissions, claims, protocol split, current fee growth | `data/fee_flow_mechanism.csv` |
| LP positions, owners, adds, and removals | `data/fee_flow_liquidity.csv` |
| Exact USDG transfers and unavailable native-flow markers | `data/fee_flow_collections.csv` |
| Hook deployers/owners and LP-controller overlap | `data/fee_flow_entities.csv` |
| First address-level onward transfers | `data/fee_flow_hops.csv` |
| Chart-ready fee step, landscape, cumulative timeline, and fee flow | `../data/fee_step.csv`, `../data/hook_landscape.csv`, `../data/cumulative_bait_fees.csv`, `../data/fee_flow_summary.csv` |
| PNG rendering | `../render_charts.py`, `../charts/` |

## Prior art

A sourced survey of earlier simulation-aware contracts, bait pools and quote poisoning, and of the defenses others adopted, is in [prior-art.md](prior-art.md).

## Multi-chain census: instruments and limits

- `scan_rpc.py --chain <name>` and `probe_hooks.py --chain <name> --all` read `chains.json`
  (PoolManager, Quoter, public RPC, deploy block per chain). `scripts/census-chain.sh <chain>`
  runs both; `CENSUS_CHUNK`, `CENSUS_PAUSE`, `CENSUS_ATTEMPTS` tune the getLogs window and the
  patience for rate-limited endpoints; `RPC_OVERRIDE` swaps the endpoint without editing config.
- `probe_quoter.py --chain <name> --undeterminable` re-tests the hooks the direct probe could
  not classify through the official V4 Quoter (`quoteExactInputSingle`, selector `0xaa9d21cb`)
  at gasPrice 0 and at base fee. `no_quote` means the swap path reverted (the decoded samples
  were `NotEnoughLiquidity`); it is not a verdict about the hook.
- `aggregate_chains.py` writes `../data/multichain_summary.csv` and `../data/multichain_bait_hooks.csv`.
  `../data/multichain_bait_pools.csv` records the per-pool follow-up (initializer, owner, swap
  counts, fee ladders) with the method and UTC time of each query.
- Coverage on 2026-09-05: Ethereum, Arbitrum, Unichain, Optimism, Avalanche complete; Base and
  Polygon partial (public endpoints throttle full `Initialize` scans); BNB not scanned (no free
  endpoint serves log ranges above 100 blocks). Never point the scan at a production RPC lane.
