Reference
Security
Trust model
| Party | Can | Cannot |
|---|---|---|
| The API | Propose a route and a minOut | Take funds. The user signs tx, and the contract enforces minOut and recipient. A malicious or buggy API could propose a bad route, but not one below the minOut the user signs |
| Executor owner | Change the fee (≤ 1%) and recipient, the WHYPE allow-list; pause swaps | Move user funds (the Executor holds none), raise the fee above 1%, block cancels |
| OrderBook owner | Choose keepers | Touch escrow; stop owners from cancelling |
| Keeper | Choose the route of a due slice | Fill below the order's floor, send output elsewhere than the owner, fill early or after expiry |
| FeeCollector keeper | Convert collected fees into the target token | Send them anywhere but the treasury |
| A pool (any, including malicious) | Execute the swap it was asked to do | Get paid in another token, get paid more than the hop's input, call back outside its own swap |
The owner is an EOA by choice (no multisig). Ownership transfers are two-step.
Contract protections
- Callback authentication with transient storage: only the pool being called can trigger payment; the token and the maximum amount are fixed before the call; the pool-relayed
datais never trusted for payments; the v4 pool key is pinned. See Executor → Callback security. - Balance-delta accounting everywhere: fee-on-transfer tokens can't make the Executor over-credit itself.
minOuton what the recipient actually receives.- Reentrancy guard on every entry point; checks-effects-interactions in the OrderBook.
- Zero balance between transactions: fees forwarded in the same transaction;
receive()restricted. - Emergency stop:
setPaused(true)blocks swaps and fills, never cancels or quotes.
API protections
- Input validation at the boundary (addresses, integers, ranges).
- Internal errors are never returned (they could contain the RPC URL or keys); logged with the request id.
- No RPC URL from the server's environment is exposed;
/configreturns the network file's public RPC. - Rate limiting per IP, per partner key, shared through Redis.
- The API serves no web page (the front is a separate project); its responses carry
x-content-type-options: nosniff. - Every transaction is simulated before it's returned.
- Fee conversion is protected against price manipulation by an EMA guard.
Internal audit
An independent read-only review of the contracts and the API, with each fix covered by a regression test:
| Severity | Finding | Fix |
|---|---|---|
| Critical | OrderBook.fill was open to anyone, and the UI created DCAs with minOut = 0: a fake "pool" could take a whole slice | create rejects a zero floor; the API requires minOut; the UI sets a DCA floor |
| High | Whoever filled kept the surplus above the floor, so a DCA always executed at its floor | Fills reserved to allow-listed keepers, plus a keeper fillerMinOut; the contract enforces max(floor, fillerMinOut). Our keeper uses 99.5% of its quote |
| Medium | unlockCallback was unauthenticated for unknown modes | Every non-quote mode requires the active PoolManager |
| Low | Callbacks paid the token and amount relayed by the pool | Expected token, max amount and v4 key in transient storage |
| Low | minOut was checked before the tax of a taxed tokenOut | Checked on the recipient's actual balance change |
| Low | Refunds could get stuck (owner can't receive HYPE, or is blacklisted) | cancelTo(id, to) |
| Low | Owner powers | Two-step ownership; feeRecipient can't be 0 or the Executor |
| Info | A taxed leg could round to 0; the probe failed on USDT-style tokens | Zero legs skipped; low-level calls in the probe |
Automated reviews of later commits, also fixed:
| Area | Finding | Fix |
|---|---|---|
| API keys | An unknown key got its 401 before the rate limiter ran, so keys could be guessed without limit | The key check runs after the limiter: wrong keys count against the caller's IP limit. Keys must be ≥ 24 characters (the API won't start otherwise) |
| Adapters | A pool or factory could report a fee outside [0, 100%); the stable-curve solver had no step cap; Solidly factories were auto-indexed without a back-pointer check | Such fees price the pool at zero; the Newton solve stops at 64 steps; a Solidly factory is only indexed when its sample pool's factory() points back to it |
| SDK | token(address) put an unchecked value in the URL path (../orders…) | Rejects anything but a 20-byte hex address before any request (pool ids too) |
| Event store | The quote-savings cache's size cap only evicted old entries | Hard cap, oldest first |
| Data routes | Price cache keyed by request input; /candles accepted any address | Size-capped caches; unknown tokens → 404 |
| List params | limit=-5 became LIMIT -5 in SQL (no limit) | Every limit/since/offset validated as a bounded integer |
| PnL | The equity curve priced every day × token (pathfinding each time) | One price query per token, 180-day window, 30 s cache per wallet |
| Self-serve keys | Free wallets → unlimited keys, each with its own bucket (Sybil) | Self-serve keys start at the public tier and share their IP's bucket; only operator-raised keys (explicit upgraded flag) get their own; 5 creations per IP per day |
| Liquidity | A contract that calls a pool's mint and answers factory() with the real factory would have been treated as a position manager, so users would approve tokens to it | Only managers configured in the network file are used; discovery only suggests candidates |
| Liquidity | A remove on a pool not yet indexed was built with zero minimums | Refused (409) until the pool's price is known |
| Behind Railway | Every request had the proxy's IP, so all users shared one per-IP bucket | TRUST_PROXY=<hops> (a hop count, so a spoofed X-Forwarded-For doesn't help) |
An earlier review fixed: XSS in the UI token list, a CDN-hosted script, error messages leaking upstream details, and fee conversions vulnerable to sandwiching.
Tests
- Fork tests on real Elysium testnet state (swaps across every venue type, orders, Permit2, fee-on-transfer, launchpad).
- Fuzz tests: exact quote, exact fee
floor(gross × bps / 10,000),minOutenforced, native round trip, taxed input across splits, every callback mode from a stranger. - Invariant campaigns (256 runs × 500 calls): the Executor always ends at zero (tokens and HYPE); each swap equals its quote; the fee is exact; the OrderBook's escrow equals the sum of open orders.
- Hostile mock venues: greedy pool, forged callback, partial fill.
Not done
- External audit. Commission one before significant volume.
- Multisig / timelock on owner functions (declined by the operator).
- On-chain oracle for keepers: the price guard lives in the API.
Reporting a vulnerability
Contact the operator privately before disclosing. Include the request id or transaction hash and the steps to reproduce.