feat(bot): cycle enumeration + per-block opportunity search, live-verified (7905) #14

Merged
buddysan merged 1 commit from rev/cycles-search into main 2026-09-16 14:14:25 +00:00
Owner

Phase 2, WP 7905 (revenue-director lane rev, PR 2 of 2; stacks on #12 quote+reserves). Full design, proofs and measurements in docs/search.md.

cycles.py

2-4 hop closed cycles from configured base tokens (DFS, no pool/intermediate-token reuse, deterministic order, max_cycles cap → truncated), cached per registry_hash (§2.3 formula; pinned 87f775b2877ab194 for pools.testnet.jsonregistry.py must match). Dedupe policy documented: reversals are distinct trades and kept; rotations cannot occur per base.

search.py (incl. CEO scope additions from docs/research/arc-arb-learnings.md §4.1)

  • Health gate: USD depth ≥ floor on both sides, sides within 10× (price coherence), price-source pool clears a stricter floor; no_price/shallow/incoherent_price/stale reasons reported.
  • Head-block seeding: driver fetches the block's Swap/Sync logs by blockHash; cycles touching touched pools are evaluated first; each Opportunity carries changed_pool_seeded, route_key (§2.3 format) and contention = {touched_pools, swaps_head_block, distinct_senders}.
  • Hop policy: 3-hop default; 4-hop only with [search.implausible_gross_bps], validated non-increasing in hop count.
  • Monotonicity pre-filter (exact): profit(x)=out(x)−x is concave with profit(0)=0 (proof in docs), so Π(1−φ)R_out/R_in ≤ 1 ⇒ never profitable — one integer product, rejects 5/8 cycles per block before sizing; subsumes the 2-probe test.
  • Sizing: integer ternary search with a proven bound (ε = Σ_i Π_{j>i} ⌈R_out/R_in⌉), verified against an exhaustive scan for all 12 cycles at block 62413099 (profitable ones within 1 unit).
  • Gas → base: exactly ceil(gas × max_fee_per_gas / 1e12) for the USDC base (verified: 0x3600…0000 is a 6-dec view of native USDC — impl has no deposit/withdraw, balanceOf == native // 1e12 on 20/20 addresses, gas payers' native delta == gasUsed × effectiveGasPrice); other bases priced conservatively via quote_in on the price-source pool. net ≤ min_profit never leaves.
  • Deadline truncation is reported (truncated, cycles_evaluated, rejected{reason}), nothing raised.

Live dry-run (LXC 140 local node, 220 consecutive blocks 62413233-62413452, base fee 20 gwei, no tx sent)

stage p50 p95 max
refresh 2 ms 5 ms 33 ms
head logs 0 ms 2 ms 4 ms
search 2 ms 6 ms 10 ms
whole block 6 ms 14 ms 42 ms

0/220 over the 500 ms budget, 0 truncations, 0 stale. 2 distinct net-positive routes every block (testnet toy pools with a real 6× cross-factory price gap): best 2-hop net 0.468034 USDC (gross 0.473234, gas 0.0052), 3-hop 0.443775. Fixtures: bot/tests/fixtures/search/liveblock_62413099.json (states + logs + ranking + brute-force + result, replayed bit-exact) and liverun3_220blocks_….json (timings + best per route).

docs/architecture.md

Based on PR #10's text (so it rebases cleanly after #10): §2.3 optional Opportunity fields (contention etc.), §4.2 verification detail + other-base pricing rule, §7 rows for Multicall3 sub-call caps, the independent fee verification, and the measured block-pass timing.

Tests: python3 -m unittest discover -s bot/tests -t . → 124 OK offline.

🤖 Generated with Claude Code

Phase 2, WP 7905 (revenue-director lane `rev`, PR 2 of 2; stacks on #12 quote+reserves). Full design, proofs and measurements in `docs/search.md`. ## cycles.py 2-4 hop closed cycles from configured base tokens (DFS, no pool/intermediate-token reuse, deterministic order, `max_cycles` cap → `truncated`), cached per `registry_hash` (§2.3 formula; pinned `87f775b2877ab194` for `pools.testnet.json` — `registry.py` must match). Dedupe policy documented: reversals are distinct trades and kept; rotations cannot occur per base. ## search.py (incl. CEO scope additions from `docs/research/arc-arb-learnings.md` §4.1) - **Health gate**: USD depth ≥ floor on both sides, sides within 10× (price coherence), price-source pool clears a stricter floor; `no_price`/`shallow`/`incoherent_price`/`stale` reasons reported. - **Head-block seeding**: driver fetches the block's Swap/Sync logs by `blockHash`; cycles touching touched pools are evaluated first; each Opportunity carries `changed_pool_seeded`, `route_key` (§2.3 format) and `contention = {touched_pools, swaps_head_block, distinct_senders}`. - **Hop policy**: 3-hop default; 4-hop only with `[search.implausible_gross_bps]`, validated non-increasing in hop count. - **Monotonicity pre-filter (exact)**: `profit(x)=out(x)−x` is concave with `profit(0)=0` (proof in docs), so `Π(1−φ)R_out/R_in ≤ 1` ⇒ never profitable — one integer product, rejects 5/8 cycles per block before sizing; subsumes the 2-probe test. - **Sizing**: integer ternary search with a proven `4ε` bound (`ε = Σ_i Π_{j>i} ⌈R_out/R_in⌉`), verified against an exhaustive scan for all 12 cycles at block 62413099 (profitable ones within 1 unit). - **Gas → base**: exactly `ceil(gas × max_fee_per_gas / 1e12)` for the USDC base (verified: `0x3600…0000` is a 6-dec *view* of native USDC — impl has no `deposit`/`withdraw`, `balanceOf == native // 1e12` on 20/20 addresses, gas payers' native delta == `gasUsed × effectiveGasPrice`); other bases priced conservatively via `quote_in` on the price-source pool. `net ≤ min_profit` never leaves. - Deadline truncation is reported (`truncated`, `cycles_evaluated`, `rejected{reason}`), nothing raised. ## Live dry-run (LXC 140 local node, 220 consecutive blocks 62413233-62413452, base fee 20 gwei, **no tx sent**) | stage | p50 | p95 | max | |---|---|---|---| | refresh | 2 ms | 5 ms | 33 ms | | head logs | 0 ms | 2 ms | 4 ms | | search | 2 ms | 6 ms | 10 ms | | whole block | **6 ms** | **14 ms** | **42 ms** | 0/220 over the 500 ms budget, 0 truncations, 0 stale. 2 distinct net-positive routes every block (testnet toy pools with a real 6× cross-factory price gap): best 2-hop net **0.468034 USDC** (gross 0.473234, gas 0.0052), 3-hop 0.443775. Fixtures: `bot/tests/fixtures/search/liveblock_62413099.json` (states + logs + ranking + brute-force + result, replayed bit-exact) and `liverun3_220blocks_….json` (timings + best per route). ## docs/architecture.md Based on PR #10's text (so it rebases cleanly after #10): §2.3 optional Opportunity fields (`contention` etc.), §4.2 verification detail + other-base pricing rule, §7 rows for Multicall3 sub-call caps, the independent fee verification, and the measured block-pass timing. Tests: `python3 -m unittest discover -s bot/tests -t .` → 124 OK offline. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
feat(bot): exact v2 quote math + Multicall3 reserve refresh (WP 7906, 7904)
All checks were successful
ci / forge-tests (pull_request) Successful in 2s
ci / python-tests (pull_request) Successful in 6s
ci / python-tests (push) Successful in 17s
ci / forge-tests (push) Successful in 23s
444426cefc
quote.py: integer UniswapV2 getAmountOut/getAmountIn with per-pool fee_bps from the
registry (never defaulted here); bit-exact against 10 real Swap events recorded from
the LXC 140 testnet node (all 4 factories, fee pinned at exactly 30 bps by the pair
k-invariant). v3/curve raise UnsupportedKind — no such pool on Arc testnet yet.

reserves.py: aggregate3 with allowFailure=true and an explicit block tag; sub-call 0 is
Multicall3.getBlockNumber() and a mismatch raises BlockMismatch (§4.8 guard); failed or
malformed sub-calls stale-mark the pool instead of failing the refresh; chunk caps per
endpoint class measured live (local 1024 ≈ 54 ms, public 256 ≈ 195 ms). Fixture is the
exact calldata + byte-identical results from both endpoints at block 62411099, cross-
checked against direct getReserves() calls.

Co-Authored-By: Claude Fable 5.1 <[email protected]>
Signed-off-by: BuddySan <[email protected]>
feat(bot): cycle enumeration + per-block opportunity search, live-verified (WP 7905)
All checks were successful
ci / python-tests (pull_request) Successful in 6s
ci / forge-tests (pull_request) Successful in 12s
ci / forge-tests (push) Successful in 5s
ci / python-tests (push) Successful in 17s
43e8dbbf35
cycles.py: 2-4 hop closed cycles from configured base tokens, DFS over the registry, no pool
or intermediate token reused, deterministic order, max_cycles cap with truncation flag,
cached per registry_hash (§2.3 formula pinned at 87f775b2877ab194 for pools.testnet.json).

search.py: per block — USD-depth + two-sided price-coherence pool health gate (price source
must clear a stricter floor), head-block Swap/Sync log seeding (touched cycles first) with a
per-route contention score, exact profit'(0) monotonicity pre-filter (concavity proof in
docs/search.md §3), integer ternary sizing with a proven 4ε error bound (checked against an
exhaustive scan), per-hop-count implausibility cap (must tighten with hops; 3-hop default),
gas priced at exactly ceil(gas × fee / 1e12) for the USDC base and via the price-source pool
for others; wall-clock deadline reports truncation; Opportunity per §2.3 plus the optional
fields noted in architecture §2.3.

config/bot.example.toml: base tokens, min_profit, slippage, gas table (Phase 4 replaces with
measured values), health floors, the 4 chain-proven 30-bps factories.

Verified: 0x3600…0000 is a 6-dec view of native USDC (impl has no deposit/withdraw;
balanceOf == native // 1e12 on 20/20 addresses); architecture §4.2/§7 rows updated on top of
PR #10's text. Live dry-run on LXC 140 over 220 consecutive blocks: block pass p50 6 ms /
p95 14 ms / max 42 ms, 0 truncations, 2 net-positive routes at 20 gwei (best 0.468 USDC on
testnet toy pools). No transaction sent.

Co-Authored-By: Claude Fable 5.1 <[email protected]>
Signed-off-by: BuddySan <[email protected]>
fortiblox-dev left a comment

automerger v2 auto-approved: all 7 gates pass. Approver: fortiblox-dev (opposite-identity rule). Gates: mergeable=true,label=auto-merge,author=buddysan,identity=ok,status_checks=not_required,signatures=upstream_BP_only,opt_in=repo_marker.

automerger v2 auto-approved: all 7 gates pass. Approver: fortiblox-dev (opposite-identity rule). Gates: mergeable=true,label=auto-merge,author=buddysan,identity=ok,status_checks=not_required,signatures=upstream_BP_only,opt_in=repo_marker.
buddysan deleted branch rev/cycles-search 2026-09-16 14:14:26 +00:00
Author
Owner

Auto-merged by forgejo-automerger at 2026-09-16 14:14:26 UTC. Approver: fortiblox-dev (opposite-identity rule, Option C upgrade 2026-05-13). Merger: buddysan via fast-forward-only (NOT admin force_merge). Criteria passed: mergeable=true,label=auto-merge,author=buddysan,identity=ok,status_checks=not_required,signatures=upstream_BP_only,opt_in=repo_marker. Branch 43e8dbbf35 merged onto main and feature branch deleted. See project_forgejo_automerger memory for rationale.

Auto-merged by **forgejo-automerger** at 2026-09-16 14:14:26 UTC. **Approver:** `fortiblox-dev` (opposite-identity rule, Option C upgrade 2026-05-13). **Merger:** `buddysan` via `fast-forward-only` (NOT admin `force_merge`). **Criteria passed:** `mergeable=true,label=auto-merge,author=buddysan,identity=ok,status_checks=not_required,signatures=upstream_BP_only,opt_in=repo_marker`. Branch `43e8dbbf35` merged onto `main` and feature branch deleted. See `project_forgejo_automerger` memory for rationale.
Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
2 participants
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
fortiblox/arc-mev-bot!14
No description provided.