Signed orders: units, domain, epochs and flags
In one paragraph. A Trion V2 order is a 14-field struct signed once with EIP-712. The signature is what authorizes the matcher to include the order in a batch and what the OrdersV2 contract re-checks on chain. Nothing about the order can be changed after signing; the only ways to stop it are expiry, on-chain cancel, or advancing an epoch. Getting units right matters because the contract checks tick, lot, price band and fee cap against the signed numbers.
The struct
Field order is the signing order and the EIP-712 type string (ORDER_TYPEHASH in overdrive/exchange/core/src/v2.ts):
Order(bytes32 marketId, bytes32 methodologyHash, address trader, bytes32 strategyId,
uint128 limitPrice, uint64 quantity, uint64 nonce, uint64 expiry,
uint32 accountEpoch, uint32 strategyEpoch, uint16 maxFeeBps,
uint8 side, uint8 tif, uint8 flags)
| Field | Meaning | How to fill it |
|---|---|---|
marketId | keccak256(abi.encode(symbol, methodologyHash, unitHash, multiplierWad, fixingMethodHash)) | Copy from the manifest (deployments/v2/<chain>.perps.json → markets.<SYM>.marketId) or compute with computeMarketId. |
methodologyHash | Binds the order to one price methodology | CANDIDATE_MARKETS[sym].methodologyHash (e.g. keccak256("CMPT_METHODOLOGY_V2")). The matcher re-reads the registry on admission and rejects an order whose marketId/methodologyHash differ from the registered identity, or whose market is currently disabled. |
trader | Signer and owner | Your address. Contract accounts are verified via EIP-1271. |
strategyId | Groups orders for scoped revocation | 0x00…00 for a plain order; any 32-byte id for a grid/strategy. |
limitPrice | WAD USD per unit, multiple of perpTick | CMPT tick 1e15 ($0.001), GRID 1e17 ($0.10). parsePerpPrice("2.500") → 2500000000000000000n. |
quantity | Integer lots; 1 lot = 1 contract = 1 underlying unit | toLots("2", config) → 2n. |
nonce | Uniqueness only | Any unused uint64; the web uses Date.now(). Replay protection is by order hash, not by nonce ordering. |
expiry | Unix seconds; must be > batch timestamp | Web uses 300 s for IOC, 24 h for GTC. |
accountEpoch | Must equal OrdersV2.accountEpoch(trader) at settlement | Read it before signing; mismatch reverts InvalidEpoch. |
strategyEpoch | Must equal OrdersV2.strategyEpoch(trader, strategyId) when strategyId ≠ 0 | 0 for plain orders. |
maxFeeBps | Highest fee/rebate you accept | Contract rejects if config.takerFeeBps > taker.maxFeeBps (or maker rebate for makers). Local config: taker 5, maker 3. |
side | 0 BUY, 1 SELL | |
tif | 0 GTC, 1 IOC, 2 FOK | |
flags | bit 0 reduce-only, bit 1 post-only; other bits reject | 2 = post-only; 1 = reduce-only; 3 = both. |
Side, Tif, OrderFlags enums and the Order type are exported from @trion/exchange-core/v2.
Domain and helpers
import { orderTypedData, hashOrder, Side, Tif } from "@trion/exchange-core/v2";
const typed = orderTypedData(chainId, ordersV2Address, order); // domain { name: "TrionOrders", version: "2", chainId, verifyingContract }
const signature = await walletClient.signTypedData({ account, ...typed });
const digest = hashOrder(chainId, ordersV2Address, order); // equals OrdersV2.hashOrder(order) on chain
verifyingContract is the per-market OrdersV2 address, so a CMPT signature cannot be replayed on another market, and a signature for one chain cannot be replayed on any other chain. EOAs must produce low-s signatures with v ∈ {27, 28} (OrdersV2._verifySignature).
Units and a worked example
Notional in collateral atoms is
notionalAtoms = quantityLots × lotWad × multiplierWad × priceWad × 10^collateralDecimals / 10^54
With lotWad = multiplierWad = 1e18, this collapses to quantity × price in dollars. Hypothetical: buy 2 CT of CMPT at $2.500 with six-decimal USDG → notionalAtoms = 2 × 2.5 × 1e6 = 5,000,000 atoms ($5.00). At the Template-A first tier (25 % IM) the initial margin is $1.25. calculateOrderPreview in overdrive/exchange/core/src/units.ts reproduces this before you sign.
What the contract checks
On settleBatch, OrdersV2.validateBatch (called only by the market's PerpetualV2) enforces, per order: not already terminal, expiry, tick, price band (±executionBps of I, 300 bps by default), epochs, signature, maxFeeBps, reduce-only against the current position, post-only cannot be the taker, and cumulative fill ≤ quantity. Each order hash gets an OrderState { filled, acceptedSeq, restedSeq, terminalReason, terminal } readable via orderState(bytes32).
Time-in-force and flags on and off chain
Matcher (EngineV2.submitOrder) | Contract | |
|---|---|---|
| GTC | Remainder rests in the book | Fills accumulate across batches until filled == quantity |
| IOC | Remainder discarded immediately | Order terminalized with reason IOC_END in the batch it appears in |
| FOK | Rejected with Fault{stage:"Fill", code:"STATE"} unless fully fillable | Must fill entirely in one batch |
| post-only | Never matched as taker | Reverts if the order appears as a taker |
| reduce-only | No margin reservation; clipped to position | Fill must reduce, never flip |
Cancelling and revoking
- Soft cancel (matcher only).
DELETE /v1/orders/:hashwithpersonal_sign("trion-cancel:<hash>"), verified against the trader of the resting order. Removes that order from the matcher's book only (200after actual removal;403wrong signer;409no longer resting;404unknown or already gone). It never touches chain state and cannot recall a fill already in a batch (see API). - Hard cancel of one order.
OrdersV2.cancel(order)from the trader address marks the hash terminal (CANCELLED) even before the matcher has seen the order. - Revoke everything.
OrdersV2.advanceAccountEpoch(newEpoch)withnewEpoch > current. Every order signed with the oldaccountEpochbecomes unsettleable; the matcher ingestsEpochAdvancedand drops them.advanceStrategyEpoch(strategyId, newEpoch)does the same for one strategy without touching other orders.
Epoch advances are irreversible and cost gas; they are the trader's guaranteed exit if the matcher is unavailable or censoring.
Strategy policies (TP / SL / OCO / TWAP)
StrategyPolicy { Order childTemplate; uint8 kind; uint8 triggerDirection; uint128 triggerMark; uint64 start, end, interval, totalQty; bytes32 ocoId } is signed under TrionStrategyPolicy v2 against the market's StrategyPolicyV2. Child IOC orders derive from the parent; they do not need separate signatures. The parent's limits, fees, epochs and reduce-only flag bind every child. The web submits policies to the matcher scheduler (EngineV2.submitPolicy); execution goes through StrategyPolicyV2.executePolicy → PerpetualV2.executeStrategyOrder. docs/RELEASE_V2.md REG-02 recorded a maker-replay finding in this path; whether it has since been remediated is not reconciled in that register (see Risks and security).
Source trail: overdrive/exchange/core/src/v2.ts (ORDER_EIP712_TYPES, orderTypedData, computeMarketId, notionalAtoms, CANDIDATE_MARKETS); overdrive/exchange/core/src/units.ts; overdrive/perps/src/v2/OrdersV2.sol; overdrive/exchange/web/src/pages/perp.ts (order construction).
Repository-owned documentation · September 2026 · Educational material, not investment advice and not an audit.
Documentation
V2 · Live on Robinhood Chain