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
| From | Transition | Trigger | Effect |
|---|---|---|---|
Open | propose | Jetton transfer with OP_PROPOSE_TRUE / OP_PROPOSE_FALSE and amount ≥ bondAmount + protocolFee | Status → Proposed. proposerBond = msg.amount - protocolFee. Fee forwarded to treasury. Deadline = now + liveness. |
Open | propose underfunded | msg.amount < bondAmount + protocolFee | Status unchanged. Full amount refunded. |
Proposed | dispute | Jetton transfer with OP_DISPUTE and amount ≥ bondAmount, before deadline | Status → Disputed. disputerBond = msg.amount. Resolver opens a case via OpenVote. |
Proposed | dispute after deadline | now >= deadline | Status unchanged. Bond refunded. |
Proposed | finalize | Anyone after now >= deadline | Status → Resolved. resolvedAnswer = proposedAnswer. Proposer's bond refunded. Consumer notified via OracleResult. |
Disputed | Resolve | From resolver address only | Status → Resolved. Winner gets own_bond + floor(loser_bond / 2). Treasury gets ceil(loser_bond / 2). Consumer notified. |
Disputed | TimeoutRefund | Anyone after now >= deadline + ARBITRATION_WINDOW | Status → 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).