Matching
This document is normative. Two implementations follow it:
matcher/(Rust) is the production matcher.packages/sdk/src/matcher.ts(TypeScript) is the independent reference.
Both must produce byte-identical result hashes for every case in fixtures/matching.json.
KasumiSettlement.sol recomputes the same hashes onchain from what actually settled.
The matcher is a pure function. Same input, same output, on any machine:
- integers only (no floating point anywhere)
- no clocks, no randomness, no environment
- the result does not depend on the order of the input arrays
- submission time inside an epoch never affects price or allocation
Kasumi earns nothing from the choice of clearing price. No step below optimises for operator revenue.
| Order | Size | Limit | Filled |
|---|---|---|---|
| Buy | 40 | 212.00 | 40 |
| Buy | 30 | 211.00 | 20 |
| Buy | 20 | 209.50 | none |
| Sell | 30 | 208.00 | 30 |
| Sell | 30 | 210.00 | 30 |
| Sell | 40 | 211.50 | none |
matchEpoch when this page is built. Volume is highest (60) for any price from 210 to 211. The oracle price 210.4 lies inside that interval, so it is the clearing price: 210.40, with 60 matched.1. Units#
| Quantity | Unit | Width |
|---|---|---|
baseAmount, minFillBase, fills |
raw base-token units | uint128 |
limitPrice, oraclePrice, clearing price |
quote raw units per base raw unit, times 1e18 | uint128 for limits |
| quote amounts | raw quote-token units | uint256 |
| deviations | basis points, BPS = 10000 |
uint16 |
PRICE_SCALE = 1e18. A product amount * price of two uint128 values fits in uint256. Sums of amounts
(tier totals, demand, supply, Vmax) can exceed 2^128, so the pro-rata product remaining * baseAmount and the
band product oraclePrice * (BPS ± d) can exceed 2^256: implementations must use arbitrary precision or at
least 512-bit intermediates there. No step may wrap or saturate silently.
Example: an 18-decimal Stock Token quoted in 6-decimal USDG at $211.40 per whole token has price
211.40 * 1e6 * 1e18 / 1e18 = 211_400_000.
Robinhood Chain Stock Tokens keep raw balances fixed across corporate actions and scale the displayed
amount through uiMultiplier. Chainlink's Stock Token feeds price one raw token (equity price times
multiplier). Kasumi prices are therefore per raw token and need no multiplier handling. Orders live for one
epoch, so a multiplier change cannot stale a resting order; the oracle is paused during the change and the
market does not trade (§3).
2. Input#
MatchInput {
epochId: uint64
markets: [{ marketId: bytes32, oraclePrice: uint256, maxOracleDeviationBps: uint16 }]
orders: [{ commitment: bytes32, marketId: bytes32, side: 0|1,
baseAmount, minFillBase, limitPrice: uint128,
allowPartialFill: bool, maxOracleDeviationBps: uint16 }]
}
side 0 is BUY (buy base, pay quote), 1 is SELL (sell base, receive quote).
Orders reaching the matcher have already passed validation (docs/PROTOCOL.md §6): decrypted, commitment
matches, signature valid, right epoch, validity window covers the settlement window, market allowlisted,
nonce unused, funds and allowance sufficient, not cancelled. baseAmount > 0, limitPrice > 0,
minFillBase <= baseAmount.
Preconditions. Violating any of them is an input error; the matcher refuses the whole input rather than guessing:
- commitments are unique across all orders of the epoch
- market ids are unique within
markets - every order (including one for an unknown market) has
sidein {0, 1},baseAmount > 0,limitPrice > 0andminFillBase <= baseAmount - every
maxOracleDeviationBpsis belowBPS
Orders are validated first. Orders whose marketId is not in markets are then ignored.
Each market is matched independently.
3. Oracle bounds#
If oraclePrice == 0 the market does not trade this epoch.
For each order let D = market.maxOracleDeviationBps and
d = D if order.maxOracleDeviationBps == 0
d = min(order.maxOracleDeviationBps, D) otherwise
The order's effective limit is its signed limit clamped to the band it accepts:
BUY : eff = min(limitPrice, floor(oraclePrice * (BPS + d) / BPS))
SELL: eff = max(limitPrice, ceil(oraclePrice * (BPS - d) / BPS))
Effective limits decide eligibility. A buy never pays more than eff; a sell never receives less than eff.
A consequence (proved in §8) is that the clearing price always lies inside the market band, which is what the
settlement contract checks.
4. Clearing price#
Repeat the following over the set of active orders (initially all orders of the market).
- If there is no active buy or no active sell, the market does not trade.
- Candidates are the distinct effective limits of active orders.
- For a price
p:- demand
Dm(p)= sum ofbaseAmountover active buys witheff >= p - supply
Sp(p)= sum ofbaseAmountover active sells witheff <= p - volume
V(p) = min(Dm(p), Sp(p))
- demand
Vmax= maximum ofVover the candidates. IfVmax == 0the market does not trade.lo= smallest candidate withV == Vmax,hi= largest candidate withV == Vmax.- Clearing price
P = clamp(oraclePrice, lo, hi): the oracle price if it lies in[lo, hi], otherwise the nearer end.
V is a step function that only changes at candidates, demand is non-increasing and supply non-decreasing,
so the set of volume-maximising prices is exactly the interval [lo, hi] and V(P) == Vmax for the chosen
P even when P is not itself a candidate. The clamp is the unique price in that interval closest to the
oracle, so no further tie-break exists.
5. Allocation#
At price P the eligible buys are active buys with eff >= P; eligible sells are active sells with
eff <= P. Each side receives exactly Vmax base in total, allocated as follows.
Sort the side by signed limitPrice, most aggressive first (buys: highest first; sells: lowest first).
Orders with equal limitPrice form a tier. Walk tiers in order with remaining = Vmax:
- If the tier's total
T <= remaining: every order in the tier is filled in full;remaining -= T. - Otherwise, if
remaining > 0, the tier is filled pro-rata:alloc_i = floor(remaining * baseAmount_i / T)rem_i = (remaining * baseAmount_i) mod T- the
remaining - sum(alloc_i)leftover units go one each to the orders with the largestrem_i; ties are broken by ascendingcommitment(compared as a 256-bit unsigned integer) remaining = 0
- Later tiers get nothing.
There is no time priority anywhere. The commitment is a salted hash, so the leftover tie-break (worth at most one raw unit per order) cannot be raced.
6. Minimum-fill constraints#
After allocating, an order is violated if it received a non-zero allocation below its own minimum:
required = baseAmount if !allowPartialFill (fill-or-kill)
required = minFillBase otherwise
violated = 0 < alloc < required
An order with zero allocation is never violated; it is simply unfilled.
If any order is violated, remove the single violated order with the largest commitment from the active
set and restart from §4. The active set strictly shrinks, so this terminates. If no order is violated the
allocation is final.
This is a deterministic heuristic, not a global optimum: finding the volume-maximising subset under all-or-nothing constraints is a knapsack problem. The rule is fixed so every implementation drops the same orders.
7. Amounts, rounding and dust#
For each order with alloc > 0 the fill is:
baseFilled = alloc
quoteAmount = ceil(alloc * P / 1e18) for a BUY (what the buyer pays)
quoteAmount = floor(alloc * P / 1e18) for a SELL (what the seller receives)
Base is conserved exactly: buyers receive in total what sellers deliver. Quote paid by buyers is at least
quote received by sellers; the difference is at most one raw quote unit per fill pair and is recorded in
KasumiSettlement.dust. It is rounding residue, published onchain, and sweepable only by the owner. It
is the only value the contract ever retains in v1.
Limits are enforced on the price, not on rounded amounts: a buy fills only if P <= limitPrice, a sell only
if P >= limitPrice. A buyer can therefore pay up to one raw quote unit more than alloc * P / 1e18
exactly, and never more than ceil(baseAmount * limitPrice / 1e18) in total.
8. Why the price is always inside the market band#
Every buy has eff <= floor(oracle * (BPS + D) / BPS) and every sell has
eff >= ceil(oracle * (BPS - D) / BPS). lo is the effective limit of some sell (volume can only rise at a
sell limit when moving up) and hi is the effective limit of some buy (volume can only fall after a buy
limit). So lowerBand <= lo <= P <= hi <= upperBand.
9. Output and hashes#
MarketResult { marketId, clearingPrice, matchedBase (= Vmax, up to 256 bits), fills[] , marketHash }
MatchResult { epochId, markets[], resultHash }
fillsare ordered by ascendingcommitment.marketscontains only markets with at least one fill, ordered by ascendingmarketId.
Hashes are keccak256 over the concatenation of 32-byte big-endian words (u256(x)), no length prefixes:
fillHash = keccak256( commitment ‖ u256(baseFilled) ‖ u256(quoteAmount) )
marketHash = keccak256( marketId ‖ u256(clearingPrice) ‖ u256(matchedBase) ‖ u256(fills.length)
‖ fillHash_0 ‖ … ‖ fillHash_{n-1} )
resultHash = keccak256( u256(epochId) ‖ u256(markets.length) ‖ marketHash_0 ‖ … )
An epoch with no fills has resultHash = keccak256(u256(epochId) ‖ u256(0)).
The matcher publishes every marketHash with KasumiEpochManager.postMatch(epochId, marketIds, marketHashes) before settling; the contract derives and stores resultHash from them.
KasumiSettlement.settleMarket recomputes the marketHash from the transfers it actually performed and
reverts unless it equals the published hash for that market. The epoch is SETTLED once every published
market has settled. The matcher can replace or withdraw the published hash of an unsettled market with
amendMarket, which is logged; resultHash keeps the original publication.
10. Fixture format#
fixtures/matching.json is an array of { name, input, expected }. All integers are decimal strings;
bytes32 values are 0x-prefixed lowercase hex. input is §2, expected is §9. Regenerate with
pnpm fixtures (TypeScript reference); the Rust test suite must then pass unchanged.