Security, Plainly
The engine has not had a third-party audit yet. This is its own review, in the open: what it must never do, the tests that check it, the risks that remain, and what the owner can and cannot change.
Summary
forge test, Sep 19, 2026: 58 passed, 0 failed, 0 skipped (52 unit, 2 fuzz, 4 invariant). Fork runs: an anvil fork of Robinhood Chain at block 66,657,028, same day. Every invariant below held in every run.
Scope
| Contract | ArbiStocks.sol | One file, no imports, no libraries. |
| Compiler | solc 0.8.28 | Via IR, optimizer 400 runs, EVM version cancun (transient storage). |
| Upgrades | none | No proxy, no delegatecall, no self-destruct. The code you read is the code that runs. |
| Chain | Robinhood Chain | Chain id 4663, an Arbitrum Nitro chain. |
| Talks to | Uniswap v3 | The Uniswap v3 factory and its pools, USDG, WETH and Robinhood stock tokens. Nothing else. |
| Holds | nothing | No balance between transactions, no allowances, no user deposits. |
| Status | engine | Deployed, block 67,744,893 |
| Reviewed by | its tests | Unit, fuzz, invariant and fork tests, listed below. No third-party audit yet. |
Invariants
What the engine must never do, how the code enforces it, and which tests check it. Test names are the ones in the suite.
| ID | Invariant | Enforced by | Checked by | Result |
|---|---|---|---|---|
| I-01 | The engine holds nothing between transactions. | Profit leaves inside arb. The engine never keeps a balance and never grants an allowance. | EngineHoldsNothing (invariant), all fork runs | HOLDS |
| I-02 | Callbacks are authenticated. | Transient expectedPool is set before every swap, checked and cleared in the callback. The first callback also checks the hash of its route. | 4 callback tests, FakeCallbackMidCycle | HOLDS |
| I-03 | Only canonical pools. | factory.getPool(token0, token1, fee) must return the pool itself. | Route_NonCanonicalPool | HOLDS |
| I-04 | Only USDG, WETH and stock tokens. | Every other token needs the stock code hash (or one the owner allowed). | Route_DisallowedToken, Owner_StockCodeGuards | HOLDS |
| I-05 | Profit or revert. | The cycle must end with more than the first pool took (Unprofitable), and the caller's share must reach minProfit (MinProfit). | Arb_Unprofitable, Arb_TooBigBecomesUnprofitable, Arb_MinProfitNotMet | HOLDS |
| I-06 | The fee never exceeds 20%. | MAX_FEE_BPS = 2000 is a constant; the constructor and setFee revert above it. | FeeWithinCap (invariant), Owner_FeeCap, Arb_MaxFee | HOLDS |
| I-07 | Payouts match the reported split. | fee = profit × feeBps / 10000 to the treasury, the rest to to; the Arb event reports both. | PayoutsMatchReportedSplit (invariant), FeeSplitAndProfit (fuzz) | HOLDS |
| I-08 | A quote is what arb realises. | quote runs the same swaps and reverts before paying the first pool, so it changes nothing. | QuoteMatchesArb (invariant), ProfitMatchesQuote (fuzz), Quote_NoStateChange_AndArbRealisesIt | HOLDS |
| I-09 | No reentry. | A transient lock shared by arb, quote and quoteMany; rescue is refused while a cycle runs. | 5 reentrancy tests | HOLDS |
| I-10 | Later hops fill completely. | A partial fill after the first pool reverts (PartialFill). The first pool may fill partly; the cycle then runs on its real amounts. | Arb_PartialFillLaterHopReverts, Arb_PartialFillFirstPoolUsesRealDeltas | HOLDS |
Test Coverage
Unit tests run the engine against mock Uniswap v3 pools and tokens: every revert path, the fee math, hostile tokens and hostile callers. Fuzz tests throw random fees, sizes and prices at the split and the quote. Invariant tests drive random sequences of cycles, quotes, fee changes and outside trades, and check four properties after every call.
| Group | What it checks | Tests | Result |
|---|---|---|---|
| Constructor | Every argument stored; zero addresses, equal quote tokens, an empty code hash and a fee over the cap rejected. | 2 | PASS |
| Cycles that pay | Same quote USDG, same quote WETH, cross quote in 3 hops, 4 hops, fee at 0, fee at the cap, a partial fill of the first pool. | 7 | PASS |
| Cycles that revert | Expired, paused, zero amount, amount too large, bad recipient, below minProfit, unprofitable, too big to pay, partial fill on a later hop. | 9 | PASS |
| Quotes | No state change and arb realises the quote; losing cycles quote; bad routes revert; works while paused; quoteMany mixed and malformed; one gas-hungry quoteMany entry cannot starve the rest; a quote under a state override with empty storage. | 8 | PASS |
| Routes | Length, repeated pool, not returning to start, start must be USDG or WETH, pool must trade the held token, non canonical pool, disallowed token. | 7 | PASS |
| Callbacks | A direct call, a canonical pool outside a cycle, a pool that never calls back, wrongly signed deltas. | 4 | PASS |
| Reentrancy | arb, quote and quoteMany from a token hook, a fake callback mid cycle, rescue mid cycle. | 5 | PASS |
| Tokens | A token that returns nothing on transfer works; one that returns false reverts. | 2 | PASS |
| Owner | Only the owner, the fee cap, the treasury, code hash guards, renounce always reverts, two step transfer, rescue. | 7 | PASS |
| Donations | Tokens sent to the engine stay put and a cycle never spends them. | 1 | PASS |
| Fuzz | Fee split and profit; profit matches the quote. 512 runs each. | 2 | PASS |
| Invariants | Engine holds nothing, payouts match the split, quote matches arb, fee within the cap. 128 runs of 64 calls each. | 4 | PASS |
forge 1.7.1, Sep 19, 2026: 58 passed, 0 failed, 0 skipped.
Fork Runs
The same engine against real Robinhood Chain pools on a local anvil fork. Each run pushes one real pool out of line with a large swap, quotes, then runs arb from a fresh account that holds no tokens at all. It checks the profit reached to, the fee reached the treasury, both match the quote, the pools converged and the engine's balance is zero.
| Run | Route (NVDA) | Hops | Gas | Profit | Spread after |
|---|---|---|---|---|---|
| A | USDG 0.05% → USDG 0.30% | 2 | 329,539 | 14.625349 USDG | 35.1 bps |
| B | WETH 0.05% → WETH 0.30% | 2 | 357,332 | 0.003705 WETH | 35.1 bps |
| C | USDG 0.05% → WETH 0.05% → WETH/USDG 0.01% | 3 | 499,105 | 93.684510 USDG | 11.0 bps |
| D | USDG 0.05% → WETH 0.05% → WETH 0.30% → USDG 0.30% | 4 | 511,560 | 3.624151 USDG | 70.3 bps |
Block 66,657,028, Sep 19, 2026. Profits come from a gap the test itself opened, not from the market. Spread after is the gross spread left between the route's pools: each landed on its fee band.
Known Risks
What the tests cannot remove. Each one is also in the docs.
Cycles are public and race each other. The one that lands second finds a smaller gap: it reverts with Unprofitable or MinProfit and its caller pays gas. Mitigation: the minProfit guard and a short deadline.
Any trade between the quote and the moment a cycle lands can shrink the gap. The cycle then pays less or reverts. It never pays out of your pocket, because nothing of yours is in it.
Robinhood's stock token contracts can pause or restrict transfers. Swaps through a paused stock revert, so every route through it reverts too. The engine holds nothing that could be stuck.
One key can pause cycles, set the fee up to 20%, change the treasury, allow a new stock code hash and rescue stray tokens (the full list is below). It cannot touch a cycle's funds or yours. Ownership moves in two steps, and renouncing it is disabled: an engine without an owner could never be paused.
setStockCode exists for the day Robinhood ships a new token proxy. A wrong hash would let other tokens onto routes. They would still need canonical Uniswap v3 pools and a profitable cycle, and the engine would still hold nothing.
The code hash check proves a token runs Robinhood's stock token proxy code, not who deployed it: anyone can deploy another copy of that proxy. That cannot hurt the engine, which holds nothing, but a route through a lookalike is the caller's own choice. The app and the keeper only build routes from the stock list in the site's config, matched by address.
Robinhood Chain has one sequencer. If it stops, no cycle settles. Pages read the chain through a read-only proxy; when it fails they show the last snapshot and say so.
The Owner's Powers
| Power | Function | Limit |
|---|---|---|
| Pause cycles | setPaused(bool) | Stops arb only. quote and quoteMany keep working. |
| Set the fee | setFee(uint16) | 0 to 2000 bps (20%) of profit. Reverts above. Today: 10%. |
| Set the treasury | setTreasury(address) | Not the zero address, not the engine itself. |
| Allow a stock code hash | setStockCode(bytes32, bool) | Adds or removes an extra hash. Cannot touch the built-in stock code hash, zero or the empty hash. |
| Rescue stray tokens | rescue(token, to, amount) | Tokens or ETH sent to the engine by mistake. Refused while a cycle runs. |
| Hand over ownership | transferOwnership, acceptOwnership | Two steps: nothing changes until the new owner accepts. |
| Renounce | renounceOwnership() | Always reverts. |
The Desk's Owner
The desk and the runner sit around the engine and never replace it: every cycle still goes through the engine's checks and still reverts if it does not pay. The runner has no owner at all. The desk's owner can:
- Allow or remove a keeper, open the desk to anyone, or pause it.
- Set the keeper bounty from 0 to 50% of each cycle's profit.
- Sweep tokens sent to the desk by mistake, and only the part above what it owes:
sweeprefuses to touch anything booked to a keeper or to the owner. - Hand ownership over in two steps. Renouncing is disabled.
The desk and the runner were each attacked by three independent reviews before deployment. Three low severity defects in the runner were confirmed, fixed and covered by tests; none could move funds.
What The Owner Cannot Do
- Change the factory, USDG, WETH or the stock code hash. They are immutables, fixed at deploy.
- Upgrade the code. There is no proxy.
- Set the fee above 20%, or take a fee from a cycle that lost.
- Take a cycle's funds or yours. The engine pays pools only inside their own callbacks, and the split at the end.
- Stop quotes. Pausing blocks
arbonly. - Move anyone's tokens. Callers approve nothing.
How It Ships
The deploy script refuses to guess. It checks the chain id, that the site's ABI and engine code match the build, that the deployer has no pending transactions and enough balance, then prints the plan.
The owner types deploy. Nothing broadcasts before that, and the script will not deploy a second engine unless told to on purpose.
Report An Issue
Found something? Send a DM to @arbistocks on X with what you found and how to reproduce it. Please give us time to fix an exploitable issue before you post it in public.
DM On X