# How the hook recognizes a simulation

This follow-up uses Robinhood Chain historical block **54,676,220**, investigated on
**September 5, 2026**. It is separate from the September 4 three-pool fee totals.
The verified Solidity source was not obtained. The analysis uses saved runtime bytecode,
call traces, executed opcode stacks, and controlled replays. The pseudocode below is a
reconstruction of an observed path, not recovered Solidity or a complete decompilation.

## What we tested

First, the census filters PoolManager `Initialize` events for dynamic-fee pools with
`beforeSwap` permission. A flag only selects candidates; it does not prove a fee trap.
We call each selected hook with the same PoolKey, swap inputs, sender, and block,
changing only the gas-price context. We decode the third return word as the fee override.
Hooks that require a complete swap are tested through the V4 Quoter. A failed call, RPC
error, or pool without liquidity is inconclusive, not a clean result.

For an incident, pin the historical state and replay the full transaction with its actual
`from`, calldata, value, and gas limit. Compare omitted fees with the mined transaction's
fee context. Then reproduce the simulator's own request shape, including its state
and code overrides. A transaction replay with a real `from` cannot validate a wrapper
that omits `from`.

The four-way wrapper experiment held state, calldata, and balances fixed:

| Origin | Fees | Output in raw token units | Meets minimum? |
|---|---|---:|---|
| Zero | Zero | 255762085697726227641860 | Yes |
| Zero | Realistic | 255762085697726227641860 | Yes |
| Broadcasting wallet | Zero | 255762085697726227641860 | Yes |
| Broadcasting wallet | Realistic | 253881086503480114968236 | No |

The minimum was `254453382332949830841960`. The final output matched the mined
transaction's reported actual output. [Download the wrapper results](data/simulation-fingerprints/wrapper-probes.csv).

## Follow the fee through every contract

The transaction was
`0x2f8711f1316d62bfd49405cccfc687f79b3d7592ebb813d2110bbfbc7c011739`.
Paired `debug_traceCall` results locate the changed return value along this path:

| Layer | Address | Operation |
|---|---|---|
| PoolManager | `0x8366a39cc670b4001a1121b8f6a443a643e40951` | Calls `beforeSwap`, selector `0x575e24b4` |
| Hook | `0xd752653d70a0ff3cc3f98ef7e690305d43550080` | `STATICCALL` to the fee controller |
| Controller | `0xd0a7c0df77eed9595f01813bd5cfdaa427250080` | `DELEGATECALL`, selector `0xa2f26dd3` |
| Implementation | `0x11817291dc8c87805e94ffe310a6fcb6a2bf46dd` | Calculates the context-dependent fee |

The [runtime manifest](data/simulation-fingerprints/runtime-manifest.json) records the block,
byte lengths, and SHA-256 hashes of the saved
[hook](data/simulation-fingerprints/0xd752653d70a0ff3cc3f98ef7e690305d43550080.hex),
[controller](data/simulation-fingerprints/0xd0a7c0df77eed9595f01813bd5cfdaa427250080.hex), and
[implementation](data/simulation-fingerprints/0x11817291dc8c87805e94ffe310a6fcb6a2bf46dd.hex).
Hashes are over decoded runtime bytes, not the hex text file.
Scanning only the hook would miss the implementation's detector.

## What the bytecode says

In the executed implementation, PCs `0x094a`–`0x09a6` combine the following conditions
with `OR`. The hard-coded address at `0x0984` is
`0x1804c8ab1f12e6bbf3894d4083f33e07309d1f38`, Foundry's default sender/origin in
[forge-std Base.sol](https://github.com/foundry-rs/forge-std/blob/master/src/Base.sol).

```text
looks_simulated =
    high_bit_of_wrapping_sub(controller.storage[3], gasleft())
    OR (tx.gasprice == 0)
    OR (opcode_0x44 == 0)
    OR (block.basefee == 0)
    OR (tx.origin == 0)
    OR (tx.origin == Foundry.DEFAULT_SENDER)

observed_path_fee = computed_fee * NOT(looks_simulated)
```

PC `0x077d` onward applies the detector to the calculated fee. A configured fixed-fee
branch exists before this path; not every configuration necessarily reaches the detector.
The evidence is the [disassembly](data/simulation-fingerprints/simulation-detector.asm)
and [executed opcode stacks](data/simulation-fingerprints/executed-detector.json).

`GASPRICE` reads the transaction gas price. `ORIGIN` reads the top-level transaction
sender, which survives nested calls; it differs from the immediate `msg.sender`.
Opcode `0x44` is exposed as difficulty or `PREVRANDAO` depending on the execution revision.
These meanings are documented in [Solidity's global variables](https://docs.soliditylang.org/en/latest/units-and-global-variables.html).

The contract has no direct “this is eth_call” flag. These are heuristics about its
execution environment. Zero-valued fields and a known test address can reveal default
simulation settings. Combining them means correcting one field can leave another bypass.

## Confirm the branches with controlled calls

Direct controller calls with realistic gas price produced:

| Origin | Fee at 300K, 3M, 30M, and 100M call gas |
|---|---|
| Zero | 0% at all four gas limits |
| Foundry default | 0% at all four gas limits |
| Broadcasting wallet | 2.4391% at all four gas limits |
| Independent nonzero control | 2.4391% at all four gas limits |

[Origin probe results](data/simulation-fingerprints/origin-probes.csv) include the raw
return data. `0x400000` is a zero-fee override; `0x405f47` adds 24,391 fee units,
or **2.4391%**. [Uniswap's LPFeeLibrary](https://github.com/Uniswap/v4-core/blob/main/src/libraries/LPFeeLibrary.sol)
defines the override flag and fee units. This is one hop's LP fee, not the full route's loss.

With a real origin and fees, changing one additional condition gave:

| Single change | Fee |
|---|---:|
| None | 2.4391% |
| Base fee set to zero | 0% |
| Difficulty set to zero | 0% |
| Controller gas threshold set to 100K | 0% |
| Timestamp advanced one second | 4.8953% |
| Block number advanced one | 2.4391% |

[Context probe results](data/simulation-fingerprints/context-probes.csv) also retain the
`random` and `prevRandao` override attempts that did not change output. Those attempts
do not prove the node applied the requested override. The `difficulty: 0` experiment
changed the effective `0x44` value and returned a zero fee.

The gas check uses the high bit of a wrapping subtraction. For normal unsigned gas
ranges, it detects remaining gas above the stored threshold. Slot 3 was
`0x3fffffffffffd`—about 1.126 quadrillion gas—so it did not explain the original 30M-gas
simulation failure. Lowering it was a diagnostic state override, not an on-chain change.

## Reproduction inputs and limits

[Probe context](data/simulation-fingerprints/probe-context.json) contains the historical
block, controller calldata, tested nonzero control, gas limit, and storage threshold.
Use an RPC that supports the historical state and tracing methods. Hold those inputs
fixed, vary one origin or fee field at a time, then inspect the same call path. The site's
live explorer performs a current zero/base-fee screen; it does not run this full matrix.

CSV rows were decoded from the preserved September 5 `rh-fee-probe.jsonl`,
`rh-context-probe.jsonl`, `rh-random-probe.jsonl`, and `rh-sim-replay-results.jsonl`.
Raw return words are retained; broadcasting wallet addresses are labeled by role in CSVs.
No live-chain mutation was made to produce the diagnostic results.

A byte occurrence alone is not proof: skip `PUSH` operands and compiler data, follow
reachable control flow and proxy calls, then confirm with executed traces and differential
probes. A suspicious opcode is a review signal; an absent opcode in one wrapper is not a
safety verdict. These findings apply to the observed implementation and configuration.

The timestamp experiment also shows why realistic simulation cannot guarantee a future
fee. Recheck through the actual simulator, quarantine demonstrated traps, invalidate cached
verdicts when code/configuration changes, and retain minimum-output checks. A faithful
single-block replay is necessary evidence, not a bound on every future block.
