Skip to main content

Assertion lifecycle

Every OMY assertion follows a fixed state machine. The lifecycle below maps to actual on-chain status codes in assertion-storage.tolk.

┌─────────────────┐
│ Open │ status = 0
└────────┬────────┘

propose() │ msg.jettonAmount >= bondAmount + protocolFee

┌─────────────────┐
┌───────────│ Proposed │ status = 1
│ └────────┬────────┘
finalize() │ │ dispute()
now >= │ │ msg.jettonAmount >= bondAmount
deadline │ ▼
│ ┌─────────────────┐
│ │ Disputed │ status = 2
│ └────────┬────────┘
│ │ ┌──────────────────────┐
│ Resolve() │ │ TimeoutRefund() │
│ from │ │ now >= deadline + │
│ resolver │ │ ARBITRATION_WINDOW │
│ ▼ ▼
│ ┌─────────┐ ┌─────────────┐
└──────────►│Resolved │ │ Cancelled │ status = 4
│status=3 │ └─────────────┘
└─────────┘

State transitions

FromTransitionTriggerEffect
OpenproposeJetton transfer with OP_PROPOSE_TRUE / OP_PROPOSE_FALSE and amount ≥ bondAmount + protocolFeeStatus → Proposed. proposerBond = msg.amount - protocolFee. Fee forwarded to treasury. Deadline = now + liveness.
Openpropose underfundedmsg.amount < bondAmount + protocolFeeStatus unchanged. Full amount refunded.
ProposeddisputeJetton transfer with OP_DISPUTE and amount ≥ bondAmount, before deadlineStatus → Disputed. disputerBond = msg.amount. Resolver opens a case via OpenVote.
Proposeddispute after deadlinenow >= deadlineStatus unchanged. Bond refunded.
ProposedfinalizeAnyone after now >= deadlineStatus → Resolved. resolvedAnswer = proposedAnswer. Proposer's bond refunded. Consumer notified via OracleResult.
DisputedResolveFrom resolver address onlyStatus → Resolved. Winner gets own_bond + floor(loser_bond / 2). Treasury gets ceil(loser_bond / 2). Consumer notified.
DisputedTimeoutRefundAnyone after now >= deadline + ARBITRATION_WINDOWStatus → Cancelled. Both bonds refunded. Protocol fee NOT refunded (already burned at propose).

Why Cancelled exists

If the resolver fails to settle a dispute within ARBITRATION_WINDOW (7 days), funds would be locked indefinitely. TimeoutRefund is the escape valve: permissionless, refunds both bonds, status moves to Cancelled. The market consumer must handle this case — see Quick Start for the recommended dedupe/cancel pattern.

Replay protection

Every transition is gated by the previous status:

// Finalize is allowed only from Proposed.
assert (st.status == STATUS_PROPOSED) throw AssertionErr.NotProposed;

This means finalize can fire exactly once. Same for Resolve (only from Disputed) and TimeoutRefund (only from Disputed). No double-resolution, no late propose after the question is settled.

Idempotent delivery

OracleResult is sent to the consumer on every resolution. Delivery is at-least-once — a network drop or aborted execution might cause the message to be re-delivered (anyone can call ReemitResult to retry). Consumers MUST dedupe by questionId. See the SDK guide for the canonical pattern.

Source

The lifecycle is implemented in tolk/contracts/Assertion.tolk with constants in assertion-storage.tolk (STATUS_OPEN = 0, STATUS_PROPOSED = 1, STATUS_DISPUTED = 2, STATUS_RESOLVED = 3, STATUS_CANCELLED = 4).