feat(contracts): ArcCycleExecutor + CREATE2 deploy + key-management design (Phase 3, WP 7908/7909/7910) #9

Closed
buddysan wants to merge 2 commits from sec/phase3-executor into main
Owner

Phase 3 — WP 7908 / 7909 / 7910 (security-director lane sec)

What lands

  • contracts/ Foundry project: ArcCycleExecutor + UniV2Adapter (kind 0) + reserved kinds 1-4 (revert UnsupportedKind), SafeTransfer, CREATE2 Deploy.s.sol with predict(), 40 tests (35 unit incl. fuzz, 3 deploy, 2 fork gated on ARC_RPC_URL), .gas-snapshot, full ABI/encoding/chain-facts in contracts/README.md.
  • docs/key-management.md — key locations, safe generation + derive-back, wiring, rotation, funding rule.
  • .gitignore — one line for forge dry-run/ simulation artefacts.

Deltas vs. the brief / architecture (I win on the Solidity side; nothing in docs/architecture.md needs changing)

  • Hop struct kept exactly as docs/architecture.md §3 (5 fields, selector 0x7328892b). The brief's "fee bps per hop or per pool" is implemented as an owner-curated per-pool registry (setPoolFee, poolFeeBps, default 30 bps) — the hot key cannot tamper with quote math, and Phase 4's encoding is unchanged.
  • forge-std is vendored (v1.16.2, 20-file closure, lib/forge-std/VENDORED.md) rather than a submodule: PR #5's forge-tests job checks out with a raw git fetch --depth 1 + checkout FETCH_HEAD (no submodule init), so a submodule would be empty in CI. Two upstream comment lines carry secret-scan:allow (full commit SHAs in URLs, token-40hex false positives). This is the one external dependency; .gitmodules was not added.
  • docs/architecture.md §2 marks fee_bps "unverified per factory": verified 30 bps on all 8 pools / both pair implementations (README §3). Project-director can flip that row.

Evidence

  • Local + LXC 140 (Foundry 1.8.1): forge test → 38 passed, 0 failed, 2 skipped (fork). On the box with ARC_RPC_URL=http://127.0.0.1:8545: test_Fork_Impl1_QuoteIsExact and test_Fork_Impl2_QuoteIsExact PASS against the real pair bytecode (received == 30-bps quote; 29 bps refused by the pair).
  • Fee proof: 4 historical Swap+Sync pairs (blocks 62380572, 62381498, 62381910, 62397678) → only numerator 997 reproduces every amountOut with floor division.
  • Live eth_call on the real node with --override-code/--override-state/--override-balance at the predicted address: USDC→ARC_TEST→USDC on 0xeDd3…/0x194c… returned 312813 = exactly the quoted profit (calldata 452 bytes).
  • Dry-run deploy on LXC 140: SIMULATION COMPLETE, predicted == deployed 0xD9ec…9aad (placeholder args), 2 693 964 gas. Nothing broadcast.
  • EVM probes: PUSH0/TSTORE/MCOPY/CLZ execute; USDC 0x3600… = 6-dec view of native balance (native_wei // 1e12).

For the CEO peer-review / a second Solidity reviewer

  1. UniV2Adapter.swap: token0/token1 set-equality check + quote from getReserves() + swap(a0,a1,to,""). Confirm the pool-to-pool to routing (nextIsV2 ? hops[i+1].pool : this) and the inputAtPool flag cannot desync for a mixed-kind path once kinds 1-4 exist (today every non-0 kind reverts before it matters).
  2. execute profit gate: balanceOf(base) before/after on address(this) — check the argument that a base token appearing mid-path (A→B→A→C→A) is routed pool-to-pool and cannot inflate balanceAfter.
  3. SafeTransfer: ret.length == 0 ? code.length == 0 : !decode(bool) — USDC facade has code (1 799 bytes) so it passes; confirm no Arc token returns malformed data.
  4. Transient reentrancy lock (bool transient) — pattern and evm_version pin (cancun) vs. Arc (Osaka-capable).
  5. onlyExecutor treats executor == address(0) as "owner-only" — check the modifier's boolean.
  6. Fee registry semantics (fee-too-low → pair K revert, fee-too-high → under-quote bounded by minOut/minProfit) — tested, but sanity-check the economic argument.
  7. Gas: 2-hop ≈ 121 k, 4-hop ≈ 204 k on mocks; no assembly, token0()/token1() read per hop (≈1.5 k/hop) — acceptable or worth a Phase 4 zeroForOne hint?

Escalations / not done

  • No key generated: the auto-mode classifier denied the cast wallet new → /etc/arc-mev-bot/*.key write (Secret-Store Writes). Runbook is in docs/key-management.md §2 — an operator step.
  • No funding, no broadcast: the only documented testnet funding path is the fortiblox gas wallet 0xAdec…2D61 (key on mainnet.fortiblox.com, not on LXC 140; 64.7 USDC). The LXC 140-local /etc/arcarb/executor.key wallet (0xCb07…A1e9) has 0 balance. No faucet URL is documented anywhere in fortiblox-arc-arb.
  • config/bot.example.toml does not exist on main; the deployed executor address will be recorded there + README §4 in a follow-up PR once deployed.

🤖 Generated with Claude Code

## Phase 3 — WP 7908 / 7909 / 7910 (security-director lane `sec`) ### What lands - `contracts/` Foundry project: `ArcCycleExecutor` + `UniV2Adapter` (kind 0) + reserved kinds 1-4 (revert `UnsupportedKind`), `SafeTransfer`, CREATE2 `Deploy.s.sol` with `predict()`, 40 tests (35 unit incl. fuzz, 3 deploy, 2 fork gated on `ARC_RPC_URL`), `.gas-snapshot`, full ABI/encoding/chain-facts in `contracts/README.md`. - `docs/key-management.md` — key locations, safe generation + derive-back, wiring, rotation, funding rule. - `.gitignore` — one line for forge `dry-run/` simulation artefacts. ### Deltas vs. the brief / architecture (I win on the Solidity side; nothing in `docs/architecture.md` needs changing) - **Hop struct kept exactly as `docs/architecture.md` §3** (5 fields, selector `0x7328892b`). The brief's "fee bps per hop or per pool" is implemented as an **owner-curated per-pool registry** (`setPoolFee`, `poolFeeBps`, default 30 bps) — the hot key cannot tamper with quote math, and Phase 4's encoding is unchanged. - **forge-std is vendored (v1.16.2, 20-file closure, `lib/forge-std/VENDORED.md`) rather than a submodule**: PR #5's `forge-tests` job checks out with a raw `git fetch --depth 1` + `checkout FETCH_HEAD` (no submodule init), so a submodule would be empty in CI. Two upstream comment lines carry `secret-scan:allow` (full commit SHAs in URLs, `token-40hex` false positives). This is the one external dependency; `.gitmodules` was not added. - `docs/architecture.md` §2 marks `fee_bps` "unverified per factory": **verified 30 bps on all 8 pools / both pair implementations** (README §3). Project-director can flip that row. ### Evidence - Local + LXC 140 (Foundry 1.8.1): `forge test` → 38 passed, 0 failed, 2 skipped (fork). On the box with `ARC_RPC_URL=http://127.0.0.1:8545`: `test_Fork_Impl1_QuoteIsExact` and `test_Fork_Impl2_QuoteIsExact` PASS against the real pair bytecode (received == 30-bps quote; 29 bps refused by the pair). - Fee proof: 4 historical Swap+Sync pairs (blocks 62380572, 62381498, 62381910, 62397678) → only numerator 997 reproduces every `amountOut` with floor division. - Live `eth_call` on the real node with `--override-code/--override-state/--override-balance` at the predicted address: USDC→ARC_TEST→USDC on `0xeDd3…`/`0x194c…` returned `312813` = exactly the quoted profit (calldata 452 bytes). - Dry-run deploy on LXC 140: `SIMULATION COMPLETE`, predicted == deployed `0xD9ec…9aad` (placeholder args), 2 693 964 gas. **Nothing broadcast.** - EVM probes: PUSH0/TSTORE/MCOPY/CLZ execute; USDC `0x3600…` = 6-dec view of native balance (`native_wei // 1e12`). ### For the CEO peer-review / a second Solidity reviewer 1. `UniV2Adapter.swap`: token0/token1 set-equality check + quote from `getReserves()` + `swap(a0,a1,to,"")`. Confirm the pool-to-pool `to` routing (`nextIsV2 ? hops[i+1].pool : this`) and the `inputAtPool` flag cannot desync for a mixed-kind path once kinds 1-4 exist (today every non-0 kind reverts before it matters). 2. `execute` profit gate: `balanceOf(base)` before/after on `address(this)` — check the argument that a base token appearing mid-path (A→B→A→C→A) is routed pool-to-pool and cannot inflate `balanceAfter`. 3. `SafeTransfer`: `ret.length == 0 ? code.length == 0 : !decode(bool)` — USDC facade has code (1 799 bytes) so it passes; confirm no Arc token returns malformed data. 4. Transient reentrancy lock (`bool transient`) — pattern and evm_version pin (`cancun`) vs. Arc (Osaka-capable). 5. `onlyExecutor` treats `executor == address(0)` as "owner-only" — check the modifier's boolean. 6. Fee registry semantics (fee-too-low → pair K revert, fee-too-high → under-quote bounded by minOut/minProfit) — tested, but sanity-check the economic argument. 7. Gas: 2-hop ≈ 121 k, 4-hop ≈ 204 k on mocks; no assembly, `token0()/token1()` read per hop (≈1.5 k/hop) — acceptable or worth a Phase 4 `zeroForOne` hint? ### Escalations / not done - **No key generated**: the auto-mode classifier denied the `cast wallet new → /etc/arc-mev-bot/*.key` write (Secret-Store Writes). Runbook is in `docs/key-management.md` §2 — an operator step. - **No funding, no broadcast**: the only documented testnet funding path is the fortiblox gas wallet `0xAdec…2D61` (key on `mainnet.fortiblox.com`, not on LXC 140; 64.7 USDC). The LXC 140-local `/etc/arcarb/executor.key` wallet (`0xCb07…A1e9`) has 0 balance. No faucet URL is documented anywhere in `fortiblox-arc-arb`. - `config/bot.example.toml` does not exist on `main`; the deployed executor address will be recorded there + README §4 in a follow-up PR once deployed. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
WP 7908 — contracts/ Foundry project (solc 0.8.30, evm cancun, forge-std
v1.16.2 vendored — see lib/forge-std/VENDORED.md). ArcCycleExecutor:
atomic 2-4 hop cycle with per-hop minOut and a balanceAfter-balanceBefore
>= minProfit gate; kind 0 UniswapV2 adapter (reserves quote, pre-push,
swap with empty data, pool-to-pool routing); kinds 1-4 reserved and
revert UnsupportedKind. Owner (two-step) / executor hot-key roles,
pause (owner or executor) / unpause (owner), withdraw / withdrawETH /
rescue, transient-storage reentrancy lock, custom errors, events for
every execution and admin action. Hop ABI is exactly docs/architecture.md
§3 (selector 0x7328892b); pool fees are an owner-curated registry
(default 30 bps) instead of calldata so the hot key cannot tamper with
quote math.

Verified live on Arc testnet (chain 5042002, LXC 140 local node): swap
fee 997/1000 on all 8 registry pools solved from 4 historical
Swap+Sync pairs and confirmed by fork tests against both real pair
runtimes; PUSH0/TSTORE/MCOPY/CLZ execute; USDC 0x3600..0000 is a
6-decimal view of the native balance; CREATE2 proxy present. A live
eth_call with code/state/balance overrides ran the real USDC->ARC_TEST->
USDC cycle through the executor and returned exactly the quoted profit.

Tests: 40 (35 unit incl. fuzz, 3 deploy/CREATE2-predict, 2 fork gated
on ARC_RPC_URL); .gas-snapshot committed. forge test passes locally and
on LXC 140 with Foundry 1.8.1.

WP 7909 — script/Deploy.s.sol: deterministic salt, predict() helper,
dry-run against the local node succeeds (2,693,964 gas). Nothing was
broadcast.

WP 7910 — docs/key-management.md: key locations (0600 under
/etc/arc-mev-bot/), safe extraction + derive-back, wiring, rotation,
funding requires explicit user go-ahead. No key generated (auto-mode
classifier denied the secret-store write; runbook is in the doc).

Co-Authored-By: Claude Fable 5.1 <[email protected]>
Signed-off-by: BuddySan <[email protected]>
buddysan force-pushed sec/phase3-executor from 196216a1af to aa9e83c53b
All checks were successful
ci / forge-tests (pull_request) Successful in 4s
ci / python-tests (pull_request) Successful in 8s
2026-09-16 13:58:14 +00:00
Compare
fortiblox-dev approved these changes 2026-09-16 14:02:33 +00:00
Dismissed
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.
Merge branch 'main' into sec/phase3-executor
All checks were successful
ci / forge-tests (pull_request) Successful in 10s
ci / python-tests (pull_request) Successful in 12s
847979f467
buddysan dismissed fortiblox-dev's review 2026-09-16 14:10:28 +00:00
Reason:

New commits pushed, approval review dismissed automatically according to repository settings

fortiblox-dev approved these changes 2026-09-16 14:14:32 +00:00
Dismissed
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.
Merge branch 'main' into sec/phase3-executor
All checks were successful
ci / forge-tests (pull_request) Successful in 4s
ci / python-tests (pull_request) Successful in 6s
2f9b2a286d
fortiblox-dev approved these changes 2026-09-16 14:21:57 +00:00
Dismissed
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.
Merge branch 'main' into sec/phase3-executor
All checks were successful
ci / forge-tests (pull_request) Successful in 4s
ci / python-tests (pull_request) Successful in 32s
0f452029bc
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.
Merge branch 'main' into sec/phase3-executor
All checks were successful
ci / forge-tests (pull_request) Successful in 5s
ci / python-tests (pull_request) Successful in 32s
7e9ac2bc54
Author
Owner

Closing as superseded: PR #16 was branched from this PR and merged first, carrying all of this PR's commits (aa9e83c contracts, plus its own e14ce41 deploy-record) into main. Diff against current main is now empty (0 files changed). Content is fully live — see contracts/, docs/key-management.md on main. Closing without merge.

Closing as superseded: PR #16 was branched from this PR and merged first, carrying all of this PR's commits (`aa9e83c` contracts, plus its own `e14ce41` deploy-record) into `main`. Diff against current main is now empty (0 files changed). Content is fully live — see `contracts/`, `docs/key-management.md` on `main`. Closing without merge.
buddysan closed this pull request 2026-09-16 14:34:37 +00:00
All checks were successful
ci / forge-tests (pull_request) Successful in 5s
ci / python-tests (pull_request) Successful in 32s

Pull request closed

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!9
No description provided.