Security review
This is the record of an internal adversarial review of the Kasumi contracts. It is not a third-party audit, and the contracts remain unaudited. The reviewer was an independent pass that had not written the code, working from the sources and the matching specification, and it proved its findings with Foundry tests where it could.
The contracts were deployed to Robinhood Chain mainnet on 2026-10-02, after this review and its fixes, at the project owner's instruction and without an external audit. An external audit is still needed. The items marked open, mitigated or accepted below apply to the deployed contracts. See DEPLOYMENT.md.
Scope: KasumiEpochManager.sol, KasumiSettlement.sol, KasumiChainlinkOracle.sol, KasumiOrderLib.sol.
The relay, the matcher, the SDK and the web app were not in scope.
Headline: no way was found for a matcher or relay to move a user's funds outside what a signed order allows. The findings were about the published result not being enforced, settlement being easy to grief, and the oracle adapter failing open.
Status words used below:
- fixed: changed in the contracts, with a regression test
- mitigated: made harder or recoverable, residual risk stated
- accepted: known limitation of v1, documented, not changed
- open: not addressed
- M-1Published result not enforcedfixed
- M-2Settlement griefingmitigated
- M-3Oracle adapter gapsmostly fixed
- L-1Delegated EOAsfixed
- L-2Unbounded return datafixed
- L-3Batch size bounded by gasopen
- L-4Commit slot, first relay winsaccepted
- L-5Commit deadline and timestampsmitigated
- L-6Receiver validationmostly fixed
Findings#
M-1. The published result hash did not constrain settlement#
postMatch stored one hash for the whole epoch and settleMarket never read it. The matcher could publish
any hash, settle any fills that passed the per-order checks, and skip finalisation. A separate finalise
step also accepted any subset of the settled markets, so an epoch could read settled while a market had
traded that was never published.
Fixed. postMatch(epochId, marketIds, marketHashes) now publishes one hash per market.
settleMarket reports the hash of the transfers it actually performed to the epoch manager, which reverts
the settlement with ResultHashMismatch unless it equals the published hash for that market. An
unpublished market cannot settle. The epoch becomes SETTLED when the count of settled markets equals the
count of published markets; the separate finalise call was removed. Tests:
test_revert_settlingADifferentResultThanPublished, test_revert_settlingAnUnpublishedMarket,
test_everyPublishedMarketMustSettle.
M-2. One participant could revert a market's batch and the epoch could then never settle#
A filled user can cancel their nonce onchain, revoke an allowance, move their balance or use a receiver that cannot take the token, between the match and the settlement. The market's batch is all-or-nothing, so it reverts. The result was write-once, so a retry without that order could never match the published hash.
Mitigated. amendMarket(epochId, marketId, newHash) lets the matcher replace or withdraw the published
result of a market that has not settled. The operator re-matches the market without the failing order and
settles the amended result. Markets are isolated from each other. Every amendment emits MarketAmended
and the original resultHash is kept. Test: test_griefedMarketCanBeRematched.
Residual risk: a griefer can force a new re-match with each further order they have in the same market, until the settlement deadline. The cost to them is gas per attempt and orders that do not fill. There is no bond or ban in v1. If the deadline passes, that market does not trade in that epoch.
M-3. Oracle adapter gaps#
No sequencer uptime check; the oraclePaused() probe failed open when the call reverted or the token had
no code; only the base token was probed; one age limit covered two feeds with very different heartbeats;
no bounds on the answer.
Fixed, with two exceptions. The adapter now returns no price, which halts the market, when: either
feed is older than its own limit (baseMaxAge, quoteMaxAge); a round is dated in the future; either
token has no code; a token flagged as a Stock Token reports oraclePaused() or the probe fails or returns
anything but a clean false; or the optional sequencer uptime feed reports the sequencer down or
restarted within the grace period. The probe is gas-capped and reads one word. Either token can be
flagged. Ownership is two-step and feeds can be removed. Tests are in KasumiChainlinkOracle.t.sol.
- Open: there is no minimum or maximum bound on the feed answer.
- Unverified: whether Chainlink publishes a sequencer uptime feed on Robinhood Chain. Without one the check is disabled and prices that were fresh before an outage are accepted as soon as the sequencer restarts, within the feed age limits.
Checked on mainnet on 2026-10-02: the AAPL Stock Token answers oraclePaused() with false; USDG has no
such function, so it must not be flagged.
L-1. EIP-7702 delegated EOAs could not trade#
Any account with code was sent down the ERC-1271 path, so a correctly signed order from an EOA that had delegated its code was rejected.
Fixed. A 65-byte signature is checked with ECDSA first; ERC-1271 is the fallback for accounts with
code. Test: test_settle_delegatedEoaSignsWithEcdsa.
L-2. Unbounded return data and gas in the ERC-1271 and pause-probe calls#
A hostile signer contract could burn the batch's gas.
Fixed. Both calls are gas-capped (200,000 and 100,000) and copy exactly one word of return data.
Test: test_revert_hostileSignerContractIsBounded.
L-3. Batch size is capped by gas#
Roughly 48k gas and about 1.15 KB of calldata per fill with simple tokens, so a market can settle a few hundred fills per epoch, fewer with heavier tokens. A market cannot be split across transactions.
Open. Chunked settlement is not implemented. The relay caps an epoch at 120 orders across all markets. The cap is per epoch, not per market, and was chosen conservatively from the rehearsal's gas figures (about 128,000 gas per additional fill with the real tokens), not from a measured block limit.
L-4. The commit slot is first-writer-wins across relays#
One compromised allowlisted relay can commit a garbage root for an epoch and it cannot be replaced. This is denial of service for that epoch, not theft.
Accepted for v1, which runs a single relay. The related gap is fixed: orderCount is now enforced, a
fill whose sequence is not below it is rejected (test_revert_sequenceBeyondOrderCount).
L-5. The commit deadline relies on the sequencer's timestamp#
With a short reveal delay, a lagging or colluding sequencer timestamp lets a commitment land after the drand key is public.
Mitigated. The deploy script's default reveal delay was raised from 6 to 12 seconds, and clients lock orders to the first drand round at or after the decryption time. Residual risk: the margin is a configuration choice, not a guarantee. A commitment anchored late still cannot make the contract settle anything users did not sign.
L-6. Receiver was not validated#
Proceeds sent to the zero address were burned and proceeds sent to the settlement contract were stranded.
Fixed for those two cases (InvalidReceiver, test_revert_receiverIsZeroOrTheContract); the SDK also
rejects a zero receiver.
- Accepted: a receiver that makes the token transfer revert, for example a blocklisted address, is one more way to cause the M-2 revert and is handled the same way.
- Accepted: tokens sent directly to the settlement contract are not recoverable.
sweepDustonly releases accounted rounding residue.
Accepted limitations of the v1 trust model#
These are not bugs in the code. They are what trusting a single matcher and owner means in v1.
- Matching is not verified onchain. The matcher chooses the clearing price inside the market band and the signed limits, which committed orders to include, and the fill sizes. The contract enforces that what settles equals what was published, not that what was published follows MATCHING.md. Deviation is detectable by recomputing the public batch.
- Free option on its own orders. The matcher can commit orders of its own before the cutoff, blind, and choose after decryption which of them to settle. It also has a last look for the whole settlement window against a moving oracle.
- Dust fills can consume an order. With
minFillBaseof 0 an order can be filled for one raw unit, which spends its nonce. - User loss is bounded by the signed limit price and the per-order oracle deviation in all of the above.
- Owner powers. The owner can replace the oracle and the matcher, change the schedule for future epochs, list and delist markets, and cancel epochs. That allows censorship and execution at a user's limit in the worst case. It does not allow taking funds beyond signed bounds.
- Partial settlement, then cancellation. If some markets settle and the epoch is then cancelled, by
the deadline or by
cancelEpoch, the settled trades are final and the remaining orders expire. The epoch readsCANCELLEDeven though some of its markets traded; indexers should readmarketSettledper market.
Checked, no issue found#
- Wrong price, overfill, expired, cancelled, other-epoch, wrong receiver and missing signature are all rejected.
- Commitment binding: the commit window, the non-replaceable root, the leaf format, leaf and node separation in the sorted-pair tree with promoted odd nodes, zero-length proofs, cross-epoch reuse.
- EIP-712 type string against the struct, dirty upper bits in calldata, signature malleability, the domain separator after a chain fork.
- Nonce bitmap: atomic with revert, no replay within a batch, across markets or across epochs.
- Accounting: base conservation, quote in at least quote out, no overflow that does anything but revert,
the balance-delta guard under donations and shared tokens,
sweepDustlimited to accounted residue. - Epoch schedule: reconfiguration does not change epochs up to the next one, schedules stay contiguous and cannot be stacked.
- Reentrancy: guarded.
Informational notes#
| Note | Status |
|---|---|
matcher is configured separately in the epoch manager and the settlement contract and can diverge |
open |
| The oracle adapter had no ownership transfer and no way to remove a feed | fixed |
setSettlement and setMatcher accept the zero address |
accepted (it disables the role) |
allowExternalRouting and maxSlippageBps are signed but unused |
accepted, documented in PROTOCOL.md |
| The schedule lookup loops over all schedules | accepted (owner-only growth) |
cancelEpoch works on future epochs |
accepted |
settleMarket reads the oracle at settlement time, not at decryption time |
accepted, documented in THREAT_MODEL.md |
| Sellers of sub-unit amounts can receive 0 quote from rounding | accepted, see MATCHING.md §7 |
Operational risks of the mainnet deployment#
These are not contract findings. They come from how the deployment is run, and none of them was in the scope of the review.
| Risk | Status |
|---|---|
| The contracts are on mainnet without a third-party audit | open |
| No trade with real tokens has settled on mainnet. One epoch with a single unfunded order was committed and settled empty. | open until a real trade settles |
| A blocklisted party or a paused Stock Token makes a transfer revert, which can stall a market for an epoch. The operator did not read the blocklist or pause state at deployment. | mitigated in the operator: blocked parties are rejected as TRANSFER_BLOCKED and paused markets are halted before publication. Verified on a fork of mainnet only. USDG is not probed. |
| The commit depends on a serverless instance, a polling client or a once-a-minute cron job being alive inside the 15-second reveal window. If the commit is missed the epoch cancels and its orders are revealed unexecuted. | open; a dedicated operator process removes the dependency |
| The live relay uses the public RPC, which rate-limits | open; use a dedicated RPC |
| The relay and matcher private keys are environment variables on the web host; the owner is a single key | open |
| The relay and matcher accounts were funded with very little ETH and are not monitored | open |
| No sequencer uptime feed is configured in the oracle adapter (M-3) | open, existence of such a feed on this chain unverified |
| Privy handles wallet login and sees login identifiers; Vercel and Neon see request metadata and stored ciphertexts | accepted, documented in THREAT_MODEL.md |
| The legal pages in the web app are unreviewed draft templates | open |
| The reveal delay (15 s) and settlement window (300 s) were not chosen from measured latency (L-5) | open |
What has not been reviewed#
The relay and its storage, the operator in packages/operator, the SDK, the Rust matcher,
the web app and its wallet integration have tests but were not part of this review. The fixes above were made after the review and have
regression tests, but they have not themselves been independently reviewed.