How the ferry runs
Everything Ferry sends, to which contract, and why — and how each claim on this site is checked against both chains.
What Ferry is
Robinhood Chain is an Arbitrum Nitro rollup that settles on Ethereum. Its bridge — the Delayed Inbox, Bridge and Outbox contracts Robinhood publishes — can deliver a message from Ethereum along with ETH. Arbitrum calls such a message a retryable ticket: a call that Robinhood Chain executes on arrival, paid for with the ETH it carried.
Ferry is a page that writes that ticket for you. Its call is a Uniswap purchase on Robinhood Chain whose recipient is your own address. You sign one Ethereum transaction; about 10 minutes later you hold the stocks. Ferry deploys no contract, holds no key and takes no fee. The whole engine is one file, js/ferry.js, which the page, the tests and the browser run all import.
The crossing
On Ethereum: one transaction
Inbox(0x1A07…7a2D).createRetryableTicket{value: deposit}(
to = SwapRouter02 on Robinhood Chain,
l2CallValue = the ETH to spend,
maxSubmissionCost = 4 × the bridge's fee at today's Ethereum base fee,
excessFeeRefundAddress = you,
callValueRefundAddress = you,
gasLimit = the estimate for THIS ticket + 30% + 60,000,
maxFeePerGas = 4 × Robinhood Chain's base fee + your gas money ÷ gasLimit,
data = SwapRouter02.multicall(deadline, [exactInput(…) per stock]))
deposit = l2CallValue + maxSubmissionCost + gasLimit × maxFeePerGas — exactly, never more
On Robinhood Chain: the ticket runs
ArbOS creates the ticket and immediately tries it: SwapRouter02.multicall with the ticket's ETH as msg.value. Each exactInput spends its share of that ETH as WETH through the stock's route — usually WETH → USDG in the 0.01% pool, then USDG → stock in the stock's deepest pool — with recipient set to your address and amountOutMinimum set to the live quote less your price limit. The shares of every leg add up to the ETH exactly, so nothing is left in the router.
Following it across
The ticket's id is Arbitrum's SubmitRetryable transaction hash: keccak256(0x69 ‖ rlp[chainId, messageNumber, aliased sender, Ethereum base fee, deposit, maxFeePerGas, gasLimit, to, l2CallValue, callValueRefund, maxSubmissionCost, feeRefund, data]). Ferry reads every field from your Ethereum receipt (the Inbox's InboxMessageDelivered and the Bridge's MessageDelivered), computes the id, and asks Robinhood Chain for that transaction. Its RedeemScheduled event names the purchase's own transaction; that receipt says what reached you.
Gas money
A new arrival on Robinhood Chain has no ETH for gas, and gas there is paid in ETH. Two facts about ArbOS (arbos/tx_processor.go in Offchain Labs' nitro) decide how Ferry delivers some:
- Before the ticket's call runs, ArbOS refunds
(maxFeePerGas − baseFee) × gasLimitto the ticket'sexcessFeeRefundAddress. Ferry sets the fee cap high on purpose, so that refund is your gas money — about 0.0003 ETH unless you choose more — and it arrives whether or not the purchase succeeds. - Any deposit above the ticket's cost stays at your aliased address (your address plus
0x1111…1111), which you cannot spend from. So Ferry's deposit is the exact sum, to the wei.
Refunds go to your address only if it has no code on Ethereum; the Inbox re-addresses refunds to contracts. Ferry therefore refuses to board from a smart-contract wallet or an EIP-7702 delegated account.
If the price moves
A crossing takes minutes, and prices move. Each purchase carries its own minimum; if any pool pays less, the whole multicall reverts and nothing is bought. The ticket then waits on Robinhood Chain for seven days with your ETH in it. Ferry's tracker shows it as Waiting and offers two things, both ordinary Robinhood Chain transactions paid from your gas money:
- Try again —
ArbRetryableTx.redeem(ticket)runs the same purchase with the same minimums. Anyone may call it. - Take the ETH —
ArbRetryableTx.cancel(ticket)pays the ticket's ETH to you (the beneficiary). Then buy on Robinhood Chain at today's price from the Cross page, in its "ETH already on Robinhood Chain" mode.
A ticket nobody touches for seven days expires and pays its ETH to you as well. The purchase also refuses to run after a three-hour deadline, so a ticket delayed by a slow sequencer cannot buy at a stale price.
Where Robinhood has a price feed for the stock and it is fresh, Ferry also refuses to board a leg priced more than 3% worse than the feed.
The way home
- Sell to ETH, one Robinhood Chain transaction:
SwapRouter02.multicall(deadline, [selfPermit(stock, …)…, exactInput(stock → … → WETH, recipient = router, minimum)…, unwrapWETH9(Σ minimums, you)]). Every Robinhood stock token and USDG accept EIP-2612 permits, so you sign a message per stock instead of sending approvals. - Send it home:
ArbSys(0x64).withdrawEth(you)with the ETH as value. Robinhood Chain records anL2ToL1Txaddressed to you. - Claim on Ethereum once the rollup has confirmed a Robinhood Chain block past your withdrawal. Ferry reads the latest confirmed assertion from the rollup (
latestConfirmed,getAssertion, itsAssertionCreatedevent), checks that the block it names carries the same send root, asks Robinhood Chain's NodeInterface for the proof (constructOutboxProof), and your wallet sendsOutbox.executeTransaction.
The confirmation period is set in the rollup: 45,818 Ethereum blocks, about 6.4 days.
What it costs
Addresses
On Ethereum — Robinhood Chain's bridge (from docs.robinhood.com)
On Robinhood Chain
How it is tested
The last run: 11/11 properties and 6,206 checks passed against live state on both chains (30 Sep 2026).
Then a sabotage sweep plants 33 bugs, one at a time, in a copy of js/ferry.js and requires the property named for each one to fail: 33/33 were caught.
The browser run: 7/7 journeys (30 checks) clicked through the real pages in Chrome with a test wallet, against private copies of both chains (30 Sep 2026).
| Property | What it proved | Checks |
|---|---|---|
| split | 400 random splits of 1–4 legs added up to the wei, and every minimum was floor(quote × (1 − limit)) exactly; 4/4 invalid splits refused | 3,401 |
| deposit | 300 random tickets: deposit = call value + submission cost + gas limit × fee cap to the wei, gas money ≥ what was asked, refunds to the owner, calldata equal to the ticket | 2,700 |
| board | the real Inbox accepted a 3-leg boarding (118,904 gas); the receipt read back to every field of the ticket, sender aliased, refunds to the owner; one wei short → InsufficientValue, fee − 1 → InsufficientSubmissionCost, a wallet with code → refunds re-addressed to its alias | 10 |
| arrive | from the aliased sender with the ticket's ETH, SwapRouter02 bought NVDA (via USDG), META (direct WETH pool) and USDG for the owner, each ≥ its minimum and within 1% of its quote, in 461,534 of the ticket's 772,448 gas; nothing to the alias, nothing left in the router; a minimum above the pool → "Too little received"; past the deadline → "Transaction too old" | 20 |
| gas | NodeInterface estimates 548,046 gas for this ticket; it carries 772,448. Fee cap 0.478 gwei over a 0.0227 gwei base fee delivers 0.000351763285646784 ETH of gas money — about 51 300k-gas transactions | 3 |
Show the other 6
| ids | 8 real tickets from the last scan: parsed from their Ethereum receipts, the computed id is the ticket Robinhood Chain created, with the same message number, deposit and aliased sender; a deposit one wei off gives an id nobody created | 26 |
| status | 6 real tickets whose purchase ran read as arrived, naming the purchase transaction and every transfer to the owner; a ticket that does not exist reads as still crossing | 14 |
| sell | 3 legs (NVDA via USDG, META via its WETH pool, USDG) sold in one transaction with 3 real permit signatures: each balance fell by exactly its amount, the owner gained 0.109424566486491103 ETH (≥ Σ minimums, within 1% of the quotes), the router kept nothing; a permit signed by another key → refused | 12 |
| send | ArbSys.withdrawEth took exactly 0.0123456789 ETH and recorded an L2ToL1Tx to the owner, which the page reads back field for field | 3 |
| claim | latest confirmed Robinhood Chain block 71,250,042 (send count 2701) matches the Outbox's own root; for 3 real withdrawals the page's proof hashes to that root and Outbox.executeTransaction answers as it must (AlreadySpent for claimed ones, success for unclaimed); one flipped proof bit → UnknownRoot | 12 |
| guard | NVDA's pool 11% above a fresh Robinhood feed → refused; at the feed's own price → boarded; ETH mixed with stocks, shares that add to 90%, and an unknown stock → all refused | 5 |
Limits and risks
- Nothing here is audited as a whole. The contracts Ferry calls are Robinhood's bridge and Uniswap's router, each audited by their authors; the page that writes the bytes is Ferry's own and is tested, not audited.
- The price can move. Then nothing is bought and your ETH waits on the ticket (see above). A limit that is too tight means more waiting tickets; one that is too loose means a worse price.
- The sequencer decides when tickets arrive. Measured at 10 minutes at the median; if Robinhood's sequencer stopped, Arbitrum's force-inclusion lets messages through after a day.
- Coming home takes 6.4 days, and the claim costs Ethereum gas.
- Tokenized stocks are Robinhood's. Robinhood can pause a stock, block an address or burn tokens; they are not shares held in your name at a broker.