Docs

How an account works

Everything Leeway does, in the order it happens — and every contract it touches, none of them ours.

The account

An account is two contracts, both made from audited code that was already on Robinhood Chain:

  • a Safe 1.4.1 (the SafeL2 singleton, made by Safe's own SafeProxyFactory) whose only owner is your wallet, with a threshold of one. It holds the USDG and the stocks.
  • a Zodiac Roles Modifier v2.1.0 (a clone of Gnosis Guild's mastercopy, made by Zodiac's ModuleProxyFactory) whose owner, avatar and target are all that Safe. The Safe enables it as a module: Roles may ask the Safe to make a call, and does so only when the caller holds a role whose rules allow that exact call.

Leeway has no contract, no admin key and no server. The page builds transactions; the chain does the rest. There are 82 Roles v2.1 modules and 2,676 Safes on Robinhood Chain (block 77,685,457, 01 Oct 2026).

Opening it: one signature, one transaction

A Safe's address is fixed by its setup and a salt before it exists, so the page can prepare everything the new account will do to itself as one Safe transaction — nonce 0 — and ask you, its future owner, to sign it (EIP-712, domain (4663, the Safe)). Then one transaction to Multicall3.aggregate3Value does, in order:

  1. SafeProxyFactory.createProxyWithNonce(SafeL2, setup([you], 1, …), salt) — the Safe, owned by you alone.
  2. Safe.execTransaction(MultiSendCallOnly, …, your signature) — the setup you signed, which:
    • makes the Roles module with ModuleProxyFactory.deployModule(Roles v2.1, setUp(safe, safe, safe), salt);
    • enables it on the Safe;
    • writes one rule per stock (below) and the daily allowances;
    • gives the trading key each stock's role;
    • approves Uniswap's router for USDG and each listed stock — only the Safe itself can make the router pull them.
  3. sends the trading key the ETH it will pay gas with.

If any step fails, nothing happens. Opening an account with ten stocks uses about 2.4 million gas.

The rule, exactly

Roles v2.1 stores conditions per (role, target, function). Each listed stock has its own role, keccak256("leeway.trade" ‖ stock), scoped to exactly one target — Uniswap SwapRouter02 — and one function, exactInputSingle, with this condition tree (written breadth-first, as Roles requires, and checked by Roles' own Integrity library when stored):

Or
├─ Calldata.Matches                       — buy
│  └─ Tuple.Matches (ExactInputSingleParams)
│     ├─ tokenIn           EqualTo   USDG
│     ├─ tokenOut          EqualTo   the stock
│     ├─ fee               EqualTo   its deepest pool's tier
│     ├─ recipient         EqualToAvatar   (the Safe itself)
│     ├─ amountIn          WithinAllowance  "leeway.buy"
│     ├─ amountOutMinimum  Pass
│     └─ sqrtPriceLimitX96 Pass
└─ Calldata.Matches                       — sell
   └─ Tuple.Matches
      ├─ tokenIn           EqualTo   the stock
      ├─ tokenOut          EqualTo   USDG
      ├─ fee               EqualTo   the same tier
      ├─ recipient         EqualToAvatar
      ├─ amountIn          WithinAllowance  "leeway.sell" ‖ stock
      ├─ amountOutMinimum  Pass
      └─ sqrtPriceLimitX96 Pass

The call is a plain CALL with no ETH (execution options None): no delegatecall, no value. Every other target is TargetAddressNotAllowed; every other function on the router is FunctionNotAllowed.

One role per stock, rather than one role for every stock, is a measured choice: Roles unpacks a function's whole condition tree on every call, and a single tree covering 41 stocks (739 conditions) made each trade cost 3.5 million gas. A role per stock is 19 conditions and about 320,000 gas.

Daily limits

Each WithinAllowance names an allowance stored in Roles: balance, refill, maxRefill, period, timestamp. Leeway sets the period to 24 hours and the balance, refill and ceiling to your limit, so the allowance starts full and refills to full once a day from the moment it was set. Roles subtracts each trade's amountIn before the trade runs and puts it back if the trade fails.

Buys share one allowance in USDG units. Each stock has its own sale allowance in that token's raw units, set from a dollar amount at the price when you set it. "No limit" is 1030 units rather than the largest number Roles can hold, because Roles refills with checked arithmetic.

A trade

The key sends Roles.execTransactionWithRole(router, 0, exactInputSingle(params), CALL, role, true). The page sets amountOutMinimum to Uniswap's own QuoterV2 quote less 1%, and the router enforces it. Roles checks the rule and the allowance, asks the Safe to make the call, and the router pays the output to the Safe. If the router refuses (the price moved), the whole transaction reverts with ModuleTransactionFailed and the allowance is untouched.

What only the owner can do

Everything else is Safe.execTransaction from your wallet, signed with Safe's "approved by sender" signature (r = you, s = 0, v = 1), which Safe accepts only when you are the sender: withdrawals (transfer to you), new limits (setAllowance), adding a key (assignRoles), removing one (assignRoles(…, false) and disableModule), adding a stock (a new rule, allowance and approval) and dropping one (revokeTarget and a cleared allowance). Roles accepts these because its owner is the Safe.

The key in your browser

"Make a key" creates an ordinary private key with the browser's own cryptographic random source and keeps it in this site's local storage. It signs trades in the page and the page sends them as raw transactions. The key holds no money — only a little ETH for gas — and is worth nothing outside your account's rules.

Any address can hold the role: a second browser, a phone, a trading bot, an AI agent, a friend's wallet. Every key shares the same daily limits.

What the page reads

To find your accounts it reads every ModuleProxyCreation log Zodiac's factory has written for the Roles mastercopy, asks each module its owner and avatar, and keeps those acting for a Safe you own. The stock list and each stock's tier are decoded from the module's own ScopeFunction events; allowances from allowances(key), accrued with Roles' own arithmetic; keys from the module's enabled-module list; activity from USDG transfer logs to and from the Safe and their receipts. Prices are each pool's slot0; quotes are QuoterV2.

Risks, plainly

  • A stolen key can trade badly. It cannot send anything out, but it can buy or sell within today's limits with any minimum it likes — including one a sandwich attacker could exploit. The daily limits are the bound on that loss. Remove a key you no longer trust.
  • Clearing the browser removes the key. The account is unaffected; make a new key from your wallet.
  • Your wallet is the account. Whoever controls the owner wallet controls everything in it, as with any Safe. Leeway does not add a recovery path.
  • The page is not audited. Safe, Zodiac Roles and Uniswap are; the code that builds the transactions is tested (below) but not reviewed by a third party.
  • Tokenized stocks are issued by Robinhood, who can pause them, block addresses and change their terms. Leeway cannot change that.

Addresses

Every contract Leeway calls, and whether its code is byte-for-byte the code at the same address on Arbitrum and Base. Uniswap's periphery and USDG are deployed per chain, so their bytes legitimately differ.

ContractAddressSame code elsewhere
USDG (Global Dollar, Paxos)0x5fc5360D0400a0Fd4f2af552ADD042D716F1d168per chain
Uniswap SwapRouter020xCaf681a66D020601342297493863E78C959E5cb2per chain
Uniswap QuoterV20x33e885eD0Ec9bF04EcfB19341582aADCb4c8A9E7per chain
Uniswap v3 Factory0x1f7d7550B1b028f7571E69A784071F0205FD2EfAper chain
Multicall30xcA11bde05977b3631167028862bE2a173976CA11yes
Safe 1.4.1 SafeProxyFactory0x4e1DCf7AD4e460CfD30791CCC4F9c8a4f820ec67yes
Safe 1.4.1 SafeL2 (singleton)0x29fcB43b46531BcA003ddC8FCB67FFE91900C762yes
Safe 1.4.1 CompatibilityFallbackHandler0xfd0732Dc9E303f09fCEf3a7388Ad10A83459Ec99yes
Safe 1.4.1 MultiSendCallOnly0x9641d764fc13c8B624c04430C7356C1C7C8102e2yes
Zodiac ModuleProxyFactory0x000000000000aDdB49795b0f9bA5BC298cDda236yes
Zodiac Roles Modifier v2.1.0 (mastercopy)0x9646fDAD06d3e24444381f44362a3B0eB343D337yes
Zodiac Roles v2.1 Integrity library0x6a6Af4b16458Bc39817e4019fB02BD3b26d41049yes
Zodiac Roles v2.1 Packer library0x61C5B1bE435391fDd7BC6703F3740C0d11728a8Cyes

Tests

The last run: 17/17 properties and 146 checks passed against live state at block 77,729,942 (01 Oct 2026). Each property opens a real Leeway account with the page's own js/leeway.js and runs its transactions with eth_simulateV1 against the real Safe, Zodiac Roles and Uniswap contracts, from test wallets with real keys. Nothing is broadcast.

Then a sabotage sweep plants 27 bugs, one at a time, in a copy of js/leeway.js — a trade paid to the wrong address, a rule that forgets the recipient, a limit written ten times too high, a key given to the wrong address — and requires the property named for each one to fail. 27/27 were caught by the property named for them.

The browser run: 11/11 journeys (40 checks) clicked through the real pages in Chrome — opening an account, adding money, trading with the browser's key, hitting a limit, adding and removing keys, withdrawing — against a private copy of the live chain (01 Oct 2026).

PropertyWhat it showsChecks
createOne signature and one transaction open an account: a Safe owned by the wallet alone, its Roles module owned by and acting for it, enabled on it, the key funded, the router approved and every allowance full.18
buyA key buys a listed stock: exactly the dollars asked leave the account, at least the floor of shares arrives in it, nothing reaches the key, and the allowance falls by exactly the dollars spent.7
sellA key sells shares it bought: exactly those shares leave, at least the floor of USDG arrives in the account, and that stock's own sale allowance falls by exactly the shares sold.7
floorThe minimum the page sets is enforced: a buy asking for more than Uniswap can give is refused (ModuleTransactionFailed), nothing moves, and the allowance is untouched.6
limit-edgeThe daily buy limit binds to the unit: one unit over today's allowance is refused by Roles, exactly the allowance goes through, and then one more unit is refused.7
refillA spent allowance stays spent for 24 hours and is full again after them — Roles' own refill, and the page's accrue() agrees with what Roles then allows.7
cannot-takeA key cannot take anything: a transfer, an approval, a swap paid to itself, a swap through another pool tier, a delegatecall and a call with ETH are each refused by the rule that names it.16
only-listedA stock that is not on the list cannot be traded under any listed stock's role, and a stranger holding no role is turned away before any rule is read.8
owner-onlyOnly the owner's wallet moves money out: the owner withdraws exactly what it asks, and the key or a stranger sending the same Safe transaction is refused by Safe.8
withdraw-all"Withdraw everything" returns every balance — the cash and each stock — to the owner, to the unit, and leaves the account empty.6
limitsThe owner can change the limits: the buy and sale allowances become exactly the new caps, and a key can then spend exactly the new buy cap.10
keysThe owner adds a key (any address) and it can trade; the owner removes the first key and it is turned away, and leaves the module's list.13
listThe owner adds a stock (rule, allowance, approval, roles for every key) and a key can trade it; the owner drops a stock and no key can trade it, and its allowance reads as cleared.8
owner-tradesThe owner's wallet can trade with no key at all: the account calls the router itself, pays exactly the dollars asked, and receives at least the floor.5
new-key-ethMaking a key from the owner's wallet later is one transaction: the key gets every listed stock's role and exactly the ETH sent with it, passed through the account.8
sale-capA sale limit set from dollars fits a sale of exactly that many dollars at today's price, and not one twice as large.6
checksThe page refuses to build what Roles would refuse or what makes no sense: no key, the owner as key, a duplicate stock, an unknown tier, a zero amount, a zero minimum.6