Market lifecycle walkthrough
Drive a full Inscribe market from creation to redemption — instantiate, bet, submit evidence, propose, challenge, vote, tally, redeem — with concrete CLI and contract calls.
This page walks the entire happy-and-challenged path of an Inscribe market against the live injective-888 deployment. Every command is real; every CID is a Cascade action_id. The walkthrough assumes you have injectived installed, two funded Injective accounts (one creator/proposer, one challenger/voter), a funded lumera1... address for Cascade uploads, and access to the deployed bond-vault and voter-registry instances listed in Contracts.
If you want the protocol-level reasoning behind each step, see Inscribe protocol. If you just want to wire up your dApp, Cascade from Injective is the place.
Prerequisites
Two roles in this walkthrough: CREATOR (who will be proposer and one voter) and CHALLENGER (who will be challenger and another voter). Use two distinct injectived keys.
Step 1 — Upload the market spec to Cascade
The spec is a JSON document describing the claim, the criteria, and the parameters. Its CID will become the market's permanent identity.
Upload through whichever Cascade path you use. The easiest in development is the inscribe-api server-signed endpoint (see Services):
Pattern A users (user-signed Lumera tx) can equivalently use the Lumera SDK Quick Start and call client.Cascade.uploader.uploadFile(file, ...) directly from their wallet. Either way you end up with a numeric action_id string.
Step 2 — Instantiate the market
Pick a settlement_block comfortably ahead of the current height. Default bonds for testnet are 1 INJ each (1000000000000000000 in base units).
Step 3 — Wire the market into the ancillary contracts
The market cannot lock bonds or sample voters until the admin has register_market-ed it on bond-vault and voter-registry.
Verify:
The market is now live and Open.
Step 4 — Place bets (state: Open)
YES and NO bets accumulate in two separate pools. The same address can hold both YES and NO stakes; each is tracked independently.
Step 5 — Submit evidence (any pre-Final state)
Evidence is a Cascade artifact (any size, any content type) annotated with a role. The market stores (cid, submitter, role, block_height).
Repeat with as many submitters and as many CIDs as you want; evidence remains accepted in Open, Proposed, Challenged, and Voting states. role must be one of press_release, tier_1_news, primary_source, other — anything else is rejected on chain.
Step 6 — Wait for settlement_block, then propose
Block height must satisfy env.block.height ≥ settlement_block for propose_verdict to succeed. In testnet that's about 100 × 0.6s ≈ a minute from instantiate, depending on testnet block time.
Author a justification (Markdown is fine), upload it, then post on-chain with exactly the proposer bond as info.funds:
The market transitions to Proposed. The bond is now locked in bond-vault under (market, Proposer, creator).
You now have two paths: uncontested finalization (Step 7a) or challenge → vote → tally (Step 7b).
Step 7a — Uncontested finalization
Wait for proposed_block + challenge_window to pass without a challenge. Then anyone can call:
Effects: proposer bond released back to proposer; market transitions to Final; winning_justification_cid set to the proposer's; final verdict = proposer's verdict. Skip ahead to Step 10 (redemption).
Step 7b — Challenge (within window)
The challenger must post a counter-verdict (≠ proposer's) and a justification.
Market transitions to Challenged. Both proposer and challenger bonds are now locked.
Step 8 — Voters register, committee draws
For a committee to be sampled, the voter set must be non-empty. Each voter registers once, paying min_bond:
Then anyone calls request_committee to sample committee_size voters and transition to Voting:
The committee is sampled deterministically from the current block hash combined with the market address and challenge block. Once drawn, each committee voter is "commitment-locked" in voter-registry and cannot unregister until the vote is tallied.
If voter_count < committee_size the sample is truncated and tally will likely resolve Invalid (no quorum reachable). Bootstrap a small voter set on testnet before running through the disputed path end-to-end; the smoke-test script in the repo (scripts/smoke-test-testnet.sh) covers the boilerplate.
Step 9 — Each committee voter casts a vote
Each voter writes a justification, uploads to Cascade, and calls cast_vote:
Repeat for each committee voter. Voters who are not on the committee will see the on-chain call rejected with NotInCommittee — the frontend at web/components/market-actions.tsx is intentionally permissive about letting users attempt the call and surfacing the error.
When all committee voters have voted (or the voting_window has expired since voting_start_block), anyone calls tally:
tally does five things atomically:
- Counts votes per side.
- Picks the winning verdict (or
Invalidif neither side reaches quorum). - Calls
voter-registry.RecordResultfor each committee voter (updates accuracy, releases commitment). - Calls
bond-vault.Slashto transfer the loser's bond to the winner. - Saves
Settlementand transitions market toFinal.
Inspect:
Step 10 — Record the canonical settlement
The on-chain Settlement knows the verdict and the winning justification CID. The canonical settlement record is a separate JSON artifact aggregating the entire trail (spec, evidence, proposer + challenger + every voter justification, final verdict, block height). Build it client-side, upload to Cascade, and attach:
MVP note. record_settlement accepts a CID from any caller in the current MVP; a contract-driven write via ICA (so the canonical record is impossible to forget) is on the roadmap. See Cascade from Injective — Pattern C. For now this is one of those rare cases where you should trust the helper service or wire your own.
Step 11 — Bettors redeem
The market is Final. Each better calls redeem on their winning side and collects:
Payout follows the formula in Contracts: pro-rata share of total pool for winners, 1:1 refund for everyone on Invalid, zero for losers. Losing-side calls are rejected with LosingSide rather than silently sending zero.
For the previous example (0.1 YES pool, 0.5 NO pool, final = YES):
- Better A's payout =
0.1 INJ × (0.1 + 0.5) / 0.1 = 0.6 INJ. - Better B's call to
redeem {"no"}is rejected.
What you just produced on Cascade
Permanently inscribed for the lifetime of this market:
| Artifact | CID | Citable as |
|---|---|---|
| Market spec | $SPEC_CID | The market's permanent identity |
| Evidence | $EV1 (and any others submitted) | Persistent citation, even if the original outlet goes dark |
| Proposer justification | $JUST | The first articulated verdict, with reasoning |
| Challenger justification | $CHAL | The counter-verdict, with reasoning |
| Voter justifications | one per committee member | Per-voter accountability |
| Canonical settlement record | $SETTLE_CID | The single document re-litigators will read first |
Everything above is fetchable forever by anyone, by GET /cascade/{cid} against inscribe-api or by GET /download/{action_id} against any cascade-aware gateway.
Frontend equivalent
Every step above has a Keplr-driven equivalent in the Inscribe web app (web/app/markets/[addr]/page.tsx and web/components/market-actions.tsx). The UI:
- Wraps
injectived tx wasm executeinclient.execute(sender, contract, msg, 'auto', memo, funds)viaSigningCosmWasmClient - Uploads to Cascade via
POST /cascade/uploadagainst the same backend used above - Refreshes after each action with a 12-second delay to give the indexer one poll-tick to catch up
The full source is small (≈ 450 lines for the actions component) and is the most concise tour of the end-to-end UX if you'd rather read code than CLI examples.