# Streembit Betting Protocol

Working draft. Implementation and validation pending. — Version 0.1

## 1. Purpose, Scope and Participants

### 1.1 Purpose

The Streembit Betting Protocol defines peer-to-peer sports wagering on the Streembit blockchain. Bettors agree to opposing positions under explicit terms and commit SBRIT to a smart contract. The contract enforces funding requirements, records accepted wagers and distributes funds according to the agreed settlement rules.

### 1.2 Scope

SBRIT is the sole wagering and settlement currency. The protocol covers wager terms, acceptance, collateral, result reporting, settlement, cancellation and recovery of funds. SSC is outside its scope.

Wagers are formed by filling standing orders; an order MAY be filled partially and by multiple counterparties, and a position MAY be reduced or closed before settlement.

The terms MUST, MUST NOT, SHOULD and MAY express mandatory requirements, prohibitions, recommendations and permitted options respectively.

### 1.3 Participants and Responsibilities

**Bettors** create or fill standing orders and fund their respective obligations. Each bettor MUST authorize the terms applicable to their participation. A fill MUST NOT be accepted unless the resulting positions are fully collateralized, taking account of any reduction or closure of existing positions.

**Betting smart contracts** verify authorizations, enforce collateral requirements, maintain orders, fills and positions, and calculate settlement entitlements. Contracts MUST NOT create unfunded payout obligations or permit the same entitlement to be claimed more than once.

**Result reporters** are validators that are ACTIVE under the validator protocol. Each market names its reporting validators and the agreement threshold in its terms. Reporters sign outcome statements; they MUST NOT have discretionary authority over collateral. A reporter fee of 1% of the settled pot is divided equally among the reporters whose signatures are used to finalize the result. The settled pot is the collateral backing the positions remaining at finalization, not cumulative betting volume. Any indivisible rounding remainder MUST be accounted for under the settlement rules.

No reporter fee is charged for a void result or timeout recovery. There is no protocol or Treasury fee.

Reporting validators’ locked deposits serve as their reporting bonds. Governance MAY slash a reporter’s deposit through the validator protocol following adjudication of a misreport. A named validator remains bonded until its reporting obligations end; validator exit mechanics belong to the validator protocol. Reporter eligibility, duties, bond retention and misreport cases are specified in Section 5.4.

**Accountable Nodes and validators** perform their ordinary consensus duties; validators additionally act as result reporters when named in a market.

**Application providers,** where present, display markets and communicate orders and transactions. Application acceptance or display alone MUST NOT establish an accepted fill; acceptance is established by the required on-chain state transition.

### 1.4 Foundational Requirements

Every market and accepted fill MUST be governed by:

- Unambiguous terms identifying the sporting event and proposition.
- Explicit obligations and sufficient committed collateral.
- A predetermined result-reporting and settlement procedure.
- Defined treatment of cancellation, unavailable results and reporting failure.
- A cancelPrice defining the payout when no outcome is determinable. The default is 50/50 between opposing positions; this is not a refund of original stakes.
- A recoveryTime after which any party MAY finalize an unfinalized market at cancelPrice.

Recovery eligibility is determined by on-chain finalization state, not by whether signatures may exist off-chain. Precedence between finalization and recovery is defined in Section 5.3.

Accepted terms MUST NOT be changed unilaterally. Every movement of wager funds MUST be attributable to an authorized protocol transition and reproducible from the recorded ledger state.

The contract’s SBRIT balance MUST cover all outstanding collateral, unpaid settlement claims and unpaid reporter fees. Any surplus (funds received outside these categories) MUST be accounted for separately and MUST NOT be used to cover them. These categories MUST NOT double-count the same obligation: finalization replaces the corresponding open-position collateral obligations with settlement and fee obligations.


### 1.5 Out of Scope

Identity/KYC, geo-restriction and licensing are allocated to the application provider and are outside this protocol’s technical scope.

Wager funds are held and released by smart-contract rules. The protocol takes no house position. Accountable Nodes do not act as betting counterparties or determine sporting outcomes; outcome reporting is performed by the validators named in the market.


## 2. Definitions, Identifiers and Prices

### 2.1 Definitions

**Event** — A real-world sporting fixture described by a JSON object (for example `sport`, `league`, `kickoff`, `team1`, `team2`). An event carries no betting terms.

**Market** — A single binary proposition about one event together with its result-reporting and settlement terms (Section 3). A market is TRUE or FALSE at finalization, or void.

**Price** — An integer in the range 0 to 1,000,000,000 (`PRICE_SCALE = 1e9`) expressing the implied probability that the market proposition is TRUE. Price 650,000,000 means 65%.

**Order** — A signed off-chain message in which a maker offers to take one side of a market at a stated price, up to a stated amount at risk (Section 4).

**Fill** — The on-chain execution of all or part of an order against a taker, producing a trade.

**Position** — The signed net amount a bettor holds in a market, in SBRIT. Positive is long (wins if TRUE), negative is short (wins if FALSE). A position of `+100` is worth `100` SBRIT if the market finalizes TRUE and `0` if FALSE.

**Reporter** — An ACTIVE validator named in a market's terms who may sign its result.

**Collateral** — SBRIT held by the betting contract backing open positions.

### 2.2 Identifiers

`H(x)` is the lowercase hex of `blake2b256` over the **canonical serialization** of a validated field object `x`. Hashes are always computed by the contract from fields it has validated under the rules below, never over a submitted JSON document as received.

| Identifier | Definition |
|---|---|
| `eventId` | `H(event fields)` |
| `marketId` | `H(market terms fields)` — the terms include `eventId`, so one event may have many markets |
| `orderHash` | `H(order fields without signature)` |
| `fillHash` | `H({maker, amount, orderGroup})` — orders sharing a `fillHash` share one fill counter (Section 4.4) |
| `reportHash` | `H({chainId, contract, marketId, outcome, purpose: "REPORT"})` — the message a reporter signs (Section 5.1) |
| `caseId` | `H({marketId, validatorId})` — at most one misreport case per pair (Section 5.4) |

`eventId` and `marketId` contain no chain or contract reference and MAY be reused across networks. Orders and reports MUST be bound to a chain and contract (Sections 4.1, 5.1).

**Canonical form.** Structured protocol data uses the shared Streembit canonical-form conventions. This protocol defines the accepted fields and enum values for each object and operation; unknown fields and invalid values MUST be rejected. Betting-specific points: the optional fields are `taker`, `acceptanceCutoff`, `proposition.subject` and `proposition.line`, omitted when unused; `reporters` order is part of the terms and fixes the fee-remainder recipient; amounts are SBRIT decimal strings with at most 8 fractional digits; `proposition.line` is a signed decimal string with at most 8 fractional digits; `price`, `nonce`, `expiry`, `reporterQuorum`, `reporterFeeBps` and all times are unsigned integers.

**Failure handling.** Non-canonical call data rejects the whole call. Once the call data is accepted, an individual order whose fields fail validation is reported as `ORDER_INVALID` and skipped by `trade` (Section 4.2); invalid market terms fail `createMarket`; an invalid outcome or report fails `finalize`.

### 2.3 Amounts and Rounding

All amounts are SBRIT decimal strings with at most 8 fractional digits. Arithmetic follows the shared contract execution standard; this protocol fixes the rounding points: round down to 8 decimals at each step marked ⌊ ⌋ below.

A trade of total size `A` (in position units) at price `P` requires:

```
A_buyer  = ⌊ A × P / 1e9 ⌋              amount at risk of the long side
A_seller = A − A_buyer                   amount at risk of the short side
```

`A_seller` is defined by subtraction so that `A_buyer + A_seller = A` exactly and the contract never holds less than the sum of positions. This is the **canonical split** of a trade. Every per-party amount in this protocol — cash flows, fill counters and balance checks — is derived from it; no other rounding of a trade amount is permitted.

At finalization with `finalPrice = F`:

```
payout_long(p)  = ⌊ p × F / 1e9 ⌋                for position p > 0
payout_short(p) = ⌊ |p| × (1e9 − F) / 1e9 ⌋      for position p < 0
```

With `F ∈ {0, 1e9}` payouts are exact. For any other `F` (void at `cancelPrice`) each position may leave at most `0.00000001` SBRIT undistributed; the remainder stays in the market's collateral and is swept to `dust` when its last position is cleared (Section 5.2). It is never paid to any party.

`MIN_TRADE = 0.0001` SBRIT. A trade in which either `A_buyer` or `A_seller` is below `MIN_TRADE` MUST be rejected with `TRADE_TOO_SMALL`.

---

## 3. Markets

### 3.1 Market Terms

A market is created on-chain by any account through `createMarket` (Section 6.1). Unlike Degens, where a match exists implicitly once referenced, Streembit markets MUST be created explicitly because contracts cannot enumerate state and because reporter eligibility is checked at creation.

| Field | Type | Rule |
|---|---|---|
| `eventId` | hex | Hash of the event JSON (Section 2.2) |
| `event` | object | The event itself, stored for display and for recovery calls. MUST contain exactly the keys `sport`, `league`, `kickoff`, `team1`, `team2` and no others; all values are strings, `kickoff` an integer ms timestamp with `kickoff ≤ blockTimestamp + MAX_ADVANCE_MS`, `MAX_ADVANCE_MS = 7776000000` (90 days), so that every reporting obligation is bounded at creation. An extension mechanism is deferred to a later version |
| `proposition` | object | `{ type, subject?, line?, rulesHash }` — see below. `rulesHash` is the hash of the grading-rules text the reporters will apply |
| `reporters` | array | Validator ids. Each MUST be ACTIVE under the validator protocol at creation. Between 1 and 7 entries, no duplicates |
| `reporterQuorum` | integer | 1 ≤ quorum ≤ `reporters.length` |
| `reporterFeeBps` | integer | MUST be `100` (1%) in this version; carried in the terms so it is part of `marketId` |
| `acceptanceCutoff` | integer ms | Optional; omitted when not used. If present, no trades are accepted at or after this timestamp. If omitted, trades are accepted until finalization (in-play betting) |
| `recoveryTime` | integer ms | MUST be greater than `event.kickoff`, greater than `acceptanceCutoff` if present, and at most `event.kickoff + MAX_RECOVERY_HORIZON_MS` with `MAX_RECOVERY_HORIZON_MS = 2592000000` (30 days) |
| `cancelPrice` | integer | 0 ≤ `cancelPrice` ≤ 1e9. Default `500000000` |

`createMarket` takes `terms`. `marketId = H(terms)`. Named reporters are not asked for consent: reporting is a mandatory duty of named validators (Section 5.4). A market whose `marketId` already exists MUST be rejected.

**Proposition types.** Every market is binary; the short side wins whenever the proposition is FALSE.

- `moneyline` — `subject` ∈ `team1` | `team2`; no `line`. TRUE if `subject` wins; FALSE if `subject` loses; draws follow the rule below. A draw is VOID unless the grading rules referenced by `rulesHash` define it as FALSE.
- `spread` — `subject` and `line` required. TRUE if `subject`'s final score + `line` is greater than the opponent's final score; FALSE if less; equal is a push and VOID. Either subject is valid: `team1 −2.5` and `team2 +2.5` are complementary propositions.
- `total` — `line` required, no `subject`. TRUE if the combined final score is greater than `line`; FALSE if less; equal is VOID.

`createMarket` MUST verify: `eventId == H(event)`; `subject` is present iff `type ∈ {moneyline, spread}` and, if present, is exactly `team1` or `team2`; `line` is present iff `type ∈ {spread, total}`.

Point spreads and totals SHOULD use half-point lines (for example `-2.5`) so that a push cannot occur. If the proposition cannot be decided under the grading rules, reporters sign the outcome `VOID`.

### 3.2 Market States

```
OPEN ──finalize (quorum)──▶ FINALIZED { outcome, finalPrice, feeWaived = (outcome == VOID) }
  └───recover (after recoveryTime)──▶ FINALIZED { outcome = VOID, finalPrice = cancelPrice, feeWaived = true }
```

Trades and fills (`trade`, `matchOrders`) are accepted only while the market is `OPEN` and, if `acceptanceCutoff` is present, before it. `claim` is accepted only when `FINALIZED`. A market never leaves `FINALIZED`. Order cancellation (`cancelGroup`, `cancelAll`) is a per-maker operation independent of any market and is accepted at any time (Section 4.4).

---

## 4. Orders and Trading

### 4.1 Order Schema

An order is a JSON object signed by the maker. Fields:

| Field | Rule |
|---|---|
| `chainId` | MUST equal the node's chain id |
| `contract` | Address of the betting contract (`cid`) |
| `marketId` | Market being bet on |
| `maker` | Maker's address. MUST equal the address derived from `makerPubKey` |
| `makerPubKey` | Compressed secp256k1 public key, 66 hex chars |
| `taker` | Optional. If present, only this address may fill the order |
| `amount` | Maximum SBRIT the maker puts at risk across all fills sharing the `fillHash` |
| `price` | Integer, 1 ≤ price ≤ 999,999,999 |
| `direction` | `buy` (maker goes long) or `sell` (maker goes short) |
| `expiry` | ms timestamp. The order is invalid at or after this time |
| `nonce` | Integer in `[0, MAX_NONCE]`. MUST equal the maker's current cancellation nonce (4.4) at fill time |
| `orderGroup` | Arbitrary string ≤ 64 chars chosen by the maker; see 4.4 |
| `signature` | secp256k1 signature by `makerPubKey` over `orderHash` (Section 2.2), using the same scheme as transaction signatures |

Orders are never submitted to the chain on their own; they travel inside the `params` of a `trade` or `matchOrders` call. An application provider MAY run an order book, a matching service or a quote service; none of these change what the contract accepts.

### 4.2 Trade Execution

A taker calls `trade` with a market, a maximum amount at risk, an optional trade expiry and a list of orders (all for that market, at most 16). The taker is `tx.from`. Orders are processed in the given sequence; an order that fails a check is skipped and reported in the result with a status code, and processing continues. The call as a whole succeeds if at least one order was filled.

For each order, the contract MUST check in this sequence:

| Status | Condition |
|---|---|
| `MARKET_FINALIZED` | Market is not `OPEN`, or `acceptanceCutoff` has passed |
| `TRADE_EXPIRED` | Taker's trade expiry ≤ `blockTimestamp` |
| `ORDER_INVALID` | Not in canonical form (2.2); chain id, contract or market mismatch; unknown or missing fields; price or nonce out of range |
| `ORDER_BAD_SIGNATURE` | Signature does not verify against `makerPubKey`, or `maker` does not match the key |
| `ORDER_EXPIRED` | `order.expiry` ≤ `blockTimestamp` |
| `ORDER_CANCELLED` | `orderGroup` cancelled, or `order.nonce ≠ cancelnonce/<maker>` |
| `INVALID_TAKER` | `order.taker` is set and ≠ `tx.from` |
| `SELF_TRADE` | `order.maker` == `tx.from` |
| `ORDER_FILLED` | Fill counter for `fillHash` ≥ `order.amount` |
| `ORDER_NO_BALANCE` | Maker's effective balance (4.3) cannot cover its side |
| `TAKER_NO_BALANCE` | Taker's remaining amount or effective balance cannot cover its side |
| `TRADE_TOO_SMALL` | Resulting `A_buyer` or `A_seller` < `MIN_TRADE` |
| `OK` | Filled |

The taker takes the opposite direction to the maker. Trade size:

```
riskFrac_long  = P / 1e9
riskFrac_short = (1e9 − P) / 1e9
makerMax = (order.amount − filled[fillHash]) / riskFrac_maker
takerMax = takerRemaining            / riskFrac_taker
A = ⌊ min(makerMax, takerMax, makerEffectiveMax, takerEffectiveMax) ⌋
```

where the two `EffectiveMax` terms are the largest `A` each party's effective balance can cover under 4.3. `A` is computed from the four maxima, then the canonical split (`A_buyer`, `A_seller`, Section 2.3) and the cash flows (4.3) are derived from it. If either party fails the balance check because of rounding, `A` is reduced by 0.00000001 once and the split and cash flows are recomputed. After any rounding or reduction of `A`, the contract MUST recheck, on the final values, both parties' effective balances, the maker's remaining order allowance (`order.amount − filled[fillHash]`), the taker's remaining amount and `MIN_TRADE` before accepting the fill; if any check fails, the order is skipped with the corresponding status.

The maker's fill counter increases by the maker's share of the canonical split: `A_buyer` if the maker is long, `A_seller` if short. The taker's remaining amount decreases by the other share.

#### 4.2.1 Execution Rules

**Batch limits.** `trade`: 1 to 16 orders. `matchOrders`: one left order and 1 to 16 right orders. `claim`: 1 to 32 addresses. `finalize`: 1 to `reporters.length` reports. An empty batch or one exceeding its limit fails the whole call before any processing.

**Provisional fills.** Each fill is computed provisionally: all checks of the status table run on the candidate values, and only when the result is `OK` are positions, fill counters, the taker's remaining amount, `collateral/<marketId>`, `openPositions/<marketId>` and balances updated. A skipped order leaves no trace in the call's state.

**Sequential state within a call.** Fills are applied in order, and every later check in the same call MUST observe the effects of earlier accepted fills, as provided by the shared contract execution standard. Two orders sharing a `fillHash` in one call therefore see the counter advanced by the first; the same maker appearing twice is charged cumulatively.

**Order identity.** The same `orderHash` MAY appear more than once in a call; each occurrence is evaluated independently against the updated state. The contract never deduplicates orders.

**Funding checks.** In `trade` the taker is checked on every fill against its in-call balance. In `matchOrders` each maker is checked per leg on its in-call balance, while the caller is checked once, after all legs, on `caller's balance before this call's fills + Σ cash_caller ≥ 0`; intermediate caller balances MAY be negative (Section 4.5).

**Claim atomicity.** `claim` processes addresses in the given sequence. A duplicate address in `addresses[]` fails the whole call with `DUPLICATE_ADDRESS`. An address with no position is reported `NO_POSITION` and skipped; every other address is paid: its payout, collateral deduction, position reset and `openPositions` decrement happen together, and the dust sweep happens only when `openPositions` reaches zero within the call. A `claim` whose entries are all `NO_POSITION` succeeds with no payouts and no state change. If the call fails at any point, no state changes.

**Netting.** Cash flows and payouts for an address that occurs in several legs or roles within one call (taker and maker-counterparty legs, the same maker twice, reporter and holder) are netted into one balance change per address, as required by the shared contract execution standard.

**Results.** `trade` and `matchOrders` return one `{ inputIndex, orderHash, status, filled }` per input order, in input order; `inputIndex` is the zero-based position in the submitted list; `orderHash` is `null` when the order was rejected before its hash could be computed (missing or malformed fields); `filled` is the maker's share of that occurrence's canonical split (the amount added to its fill counter) and `"0"` when the order was skipped. `claim` returns one `{ address, status, net }` per input address, in input order; `status` is `OK` (position cleared, `net` paid, possibly `"0"` for a losing position) or `NO_POSITION` (`net = "0"`).

### 4.3 Positions, Cash Flow and Effective Balance

A trade of size `A` at price `P` updates positions:

```
long side:   p_new = p_old + A
short side:  p_new = p_old − A
```

For each party, "closing" means reducing the magnitude of an opposite-sign prior position and "opening" means the remainder:

```
Long side:   closeAmt_long  = min(A, max(0, −p_old))     openAmt_long  = A − closeAmt_long
Short side:  closeAmt_short = min(A, max(0,  p_old))     openAmt_short = A − closeAmt_short
```

Cash flows reuse the canonical split of Section 2.3 and involve no further rounding. Each party is charged its share of the split for the whole trade and is refunded, in full, the position units it closes:

```
required   = A − closeAmt_long − closeAmt_short     (exact: change in contract holdings)
cash_long  = closeAmt_long  − A_buyer
cash_short = closeAmt_short − A_seller
```

Conservation is exact for opening, closing and reversing trades: `cash_long + cash_short = −required`. For a pure opening trade (`p_old = 0` on both sides) this reduces to `cash_long = −A_buyer` and `cash_short = −A_seller`.

Positive cash is credited to the party's SBRIT account; negative cash is debited. A party's **effective balance** for a trade is its spendable SBRIT balance plus the credit part of its cash flow; the trade is only valid if `balance + cash ≥ 0` for both parties, evaluated on the rounded values above. This is what allows an existing position to serve as collateral for an opposite-direction trade without new funds. The one exception is the caller of `matchOrders`, who is checked once on its final net balance across all legs rather than per leg (Sections 4.2.1 and 4.5).

Invariants the contract MUST maintain after every trade:

1. The sum of all positions in a market is zero.
2. For each market, `collateral/<marketId>` equals Σ positive positions while `OPEN` (updated by `+required` on every trade) and equals the unpaid remainder after finalization. The contract's SBRIT balance equals Σ `collateral/*` + `caseBonds` + `dust` + `surplus`.
3. `cash_long + cash_short = −required` exactly.

The contract also maintains `openPositions/<marketId>`, the count of non-zero positions in the market: incremented when a position leaves zero and decremented when it returns to zero, in trades and in claims alike.

Because a taker may fill several orders in one call, all cash flows for an address within a call are netted into one balance change, as required by the shared contract execution standard.

### 4.4 Cancellation and Fill Counters

An order ceases to be fillable when any of the following holds:

- The market is not `OPEN`, or `acceptanceCutoff` is present and `blockTimestamp ≥ acceptanceCutoff` (Section 3.2).
- `expiry` has passed.
- The maker has called `cancelGroup(orderGroup)`; the contract stores `cancel/<maker>/<orderGroup>`.
- The maker has called `cancelAll()`; the contract increments `cancelnonce/<maker>` (initially 0). An order is fillable only if `order.nonce == cancelnonce/<maker>`. This invalidates every order signed under the previous nonce, whatever its other fields. An order pre-signed with a higher nonce becomes fillable only when the maker's nonce reaches it, and a further `cancelAll` invalidates it in turn. The nonce is strictly increasing, so no later-included transaction can reactivate orders. `MAX_NONCE = 9007199254740991`. A `cancelAll` that would increment the nonce beyond `MAX_NONCE` MUST be rejected; the nonce never wraps. An order whose `nonce` is not an integer in `[0, MAX_NONCE]` is rejected as `ORDER_INVALID`.

Orders with the same `maker`, `amount` and `orderGroup` share a `fillHash` and therefore one fill counter, even if they differ in market, price or direction. A maker can use this to cap total exposure across several markets, or to replace a short-lived order with a fresh one without doubling exposure. A replacement with a worse price is unsafe because the earlier order remains fillable.

### 4.5 Match Orders

`matchOrders(marketId, leftOrder, rightOrders[])` is an atomic composite of fills in which the caller is taker on every leg and ends with its position unchanged. It exists for arbitrage between order books.

1. **Constraints.** All orders are for `marketId`, which MUST be `OPEN` and before `acceptanceCutoff`. Every right order MUST have the opposite `direction` to the left order. The left maker, every right maker and the caller MUST all be distinct addresses. At most 16 right orders. Every order MUST pass all checks of Section 4.2, except that the caller's funding is checked once on its final net balance under item 4 and Section 4.2.1 (intermediate caller balances MAY be negative); unlike `trade`, any failure fails the whole call with that status and no state change.
2. **Sizing.** Right orders are processed in the given sequence. For right order *i*: `A_i = ⌊ min(rightMakerMax_i, rightMakerEffectiveMax_i, leftRemainingMax, leftMakerEffectiveMax) ⌋`, where `leftRemainingMax` is the left maker's remaining allowance after legs 1..i−1 divided by its `riskFrac`. Any `A_i` whose canonical split falls below `MIN_TRADE` fails the call with `TRADE_TOO_SMALL`. `A = Σ A_i`; the left leg has size exactly `A`, so no residual position is created and the caller's position is unchanged. After `A` is known, the contract MUST recompute the left leg's canonical split for size `A` and recheck the left maker's rounded contribution against its remaining allowance, its effective balance and `MIN_TRADE`, measured against the values as they stood before this call's provisional matching allocations so that those allocations are not counted twice; if any check fails, the whole call fails with the corresponding status.
3. **Execution.** Legs are applied sequentially with positions updated between them: first the left leg of size `A` (the caller takes the opposite side of the left maker), then each right leg `A_i` (the caller takes the opposite side of right maker *i*). Each leg uses the canonical split and the cash-flow rules of Section 4.3 at that leg's order price. Each maker's fill counter increases by its share of its own leg's split; the caller has no order and no counter.
4. **Funding.** The caller's cash is the sum of its leg cash flows. The call succeeds only if `callerBalance + Σ cash_caller ≥ 0`, otherwise it fails with `TAKER_NO_BALANCE`. Apart from rounding, the caller's net cash is positive when the buy order(s) are priced above the sell order(s) — the caller buys at the lower price and sells at the higher — and negative otherwise, in which case the caller funds the difference. Maker balance checks are the same as in `trade`.
5. **Balance changes.** As in Section 4.3, one netted balance change per address for the whole call.
6. **Result.** `result` lists `{ inputIndex, orderHash, status: OK, filled }` for the left order (`inputIndex = 0`) and for each right order (`inputIndex = 1..n`), as defined in Section 4.2.1.

---

## 5. Settlement

### 5.1 Reports and Finalization

A report is a signature by a named reporter's validator signing key registered under the validator protocol over

```
reportHash = H({ chainId, contract: cid, marketId, outcome, purpose: "REPORT" })
```

where `outcome` is exactly one of the strings `"TRUE"`, `"FALSE"` or `"VOID"` (case-sensitive, no whitespace). The contract MUST reject a `finalize` call carrying any other value before verifying signatures. Reporters sign the outcome, never a price. The contract derives `finalPrice` from the outcome: `TRUE → 1e9`, `FALSE → 0`, `VOID → cancelPrice`.

Anyone may call `finalize` with a market id, an `outcome` and a list of `{ validatorId, signature }`. The contract MUST:

1. Require the market to be `OPEN`.
2. Accept a signature only if the validator is named in the market's `reporters`, appears once, and is eligible to sign under Section 5.4 item 2 (status `ACTIVE` or `UNBONDING`).
3. Verify each accepted signature against `reportHash` for the submitted `outcome`.
4. Require the number of valid signatures ≥ `reporterQuorum`.
5. Finalize atomically. In the same call the contract MUST (i) record `{ outcome, finalPrice, finalizedAt: blockTimestamp, finalizedBy: "QUORUM", reporters: [valid signers in market order], feeWaived: outcome == VOID, fee_total }`, set the state to `FINALIZED`, decrement `reporterOpenMarkets/<vid>` and set `reporterLastFinalizedAt/<vid> = blockTimestamp` for every validator named in the market's `reporters` (signer or not); (ii) compute `fee_total = ⌊ collateral/<marketId> × reporterFeeBps / 10000 ⌋` (`0` when `feeWaived`) and deduct it from `collateral/<marketId>`; (iii) credit each recorded reporter `⌊ fee_total / n ⌋` at its reward address registered under the validator protocol, with the indivisible remainder credited to the first recorded reporter. All three succeed together or the call fails with no state change.

Reporter fees are paid once, at finalization; `claim` never pays reporters. A reporter who did not sign receives nothing. The fee is 1% of the settled pot rounded down to eight decimals.

### 5.2 Claims and Reporter Fee

Anyone may call `claim` with a market id and one or more addresses. For each address with a non-zero position:

```
gross = payout_long(p)  if p > 0,  payout_short(p)  if p < 0        (Section 2.3)
net   = feeWaived ? gross : ⌊ gross × (10000 − reporterFeeBps) / 10000 ⌋
```

`net` is credited to the position holder and deducted from `collateral/<marketId>`; the position is set to zero and `openPositions/<marketId>` is decremented. When `openPositions/<marketId>` reaches zero, the remaining `collateral/<marketId>` is transferred to `dust` and set to zero. Because `fee_total` and every `net` are rounded down, Σ `net` + `fee_total` ≤ settled pot always holds. For non-VOID winning claims, the deduction is 1% of `gross` plus a rounding excess of less than 0.00000001 SBRIT (possibly zero); VOID claims carry no fee.

A position that has been set to zero cannot be claimed again. Losing positions (gross = 0) are simply zeroed.

### 5.3 Recovery

Anyone may call `recover` with a market id. The contract MUST reject it unless the market is `OPEN` and `blockTimestamp ≥ recoveryTime`. On success the market becomes `FINALIZED` with `outcome = VOID`, `finalPrice = cancelPrice`, `finalizedAt = blockTimestamp`, `finalizedBy = "RECOVERY"`, `feeWaived = true`, `fee_total = 0` and an empty reporter list (`openCase` measures its window from this `finalizedAt`); `reporterOpenMarkets/<vid>` is decremented and `reporterLastFinalizedAt/<vid>` set to `blockTimestamp` for every validator named in the market's `reporters`.

Precedence is by inclusion order: whichever of `finalize` or `recover` is applied first stands, and the other is rejected with `MARKET_FINALIZED`. Reports signed before `recoveryTime` but submitted after a successful `recover` have no effect.

### 5.4 Reporter Lifecycle

Reporters are validators; their status is the one recorded under the validator protocol.

1. **Eligibility at creation.** Every named reporter MUST be `ACTIVE` when `createMarket` runs (Section 3.1).
2. **Eligibility at signing.** At `finalize`, a named reporter's signature counts only if its status at that time is `ACTIVE` or `UNBONDING` (still bonded). Signatures from reporters in `SLASHED`, `EXITED` or any other status are ignored. If the remaining eligible reporters cannot reach `reporterQuorum`, the market resolves through `recover` at `recoveryTime`; there is no other remedy.
3. **Signing key.** Markets store validator ids, not keys. Reports are verified against the validator's signing key registered under the validator protocol at the time of finalization; a report signed with a key that is no longer registered is not counted and MUST be re-signed.
4. **Duty.** Reporting is mandatory for named validators. Any validator that is `ACTIVE` at creation MAY be named without its consent; once named it MUST produce a valid report for the market before `recoveryTime`. Validators not named in a market have no obligation to it. Market terms bound every obligation in time (Section 3.1: `MAX_ADVANCE_MS`, `MAX_RECOVERY_HORIZON_MS`).
5. **Bond retention.** The contract maintains `reporterOpenMarkets/<vid>`, the number of `OPEN` markets naming the validator (`+1` at `createMarket`, `−1` at `finalize` or `recover`); `reporterLastFinalizedAt/<vid>`, set to `blockTimestamp` whenever a market naming the validator is finalized or recovered, whether or not it signed; and `reporterOpenCases/<vid>`, the number of unresolved reporter cases against it (item 7). A named validator's reporting obligations are outstanding while `reporterOpenMarkets/<vid> > 0`, while `blockTimestamp < reporterLastFinalizedAt/<vid> + CHALLENGE_WINDOW_MS` with `CHALLENGE_WINDOW_MS = 604800000` (7 days), or while `reporterOpenCases/<vid> > 0`. A validator with outstanding reporting obligations remains bonded; the validator protocol reads these three records and is responsible for deposit release.
6. **Finality.** A finalization is never reversed: trades, finalization and claims are final. Slashing is a penalty on the reporter, not compensation to bettors. Reporters cannot be added to or removed from a market after creation; the reporter list is part of `marketId`.
7. **Reporter cases.** A case has one of two grounds: `MISREPORT` (a signature used in the finalization attests a wrong outcome under the grading rules) or `NON_REPORTING` (the named validator did not publish a valid report before `recoveryTime`). An absent signature in the finalization is not by itself proof of non-reporting: a submitter may omit an available signature, and recovery can occur despite published reports. Governance assesses the evidence for either ground. `caseId = H({ marketId, validatorId })`; at most one case exists per pair. Constants: `CASE_BOND = 100` SBRIT, `CASE_RESOLUTION_MS = 2592000000` (30 days). Case states: `OPEN → DISMISSED | UPHELD_PENDING_PENALTY | TIMED_OUT`; `UPHELD_PENDING_PENALTY → CLOSED | TIMED_OUT`. The case record stores `{ marketId, validatorId, ground, opener, openedAt, deadline = openedAt + CASE_RESOLUTION_MS, evidence[], status }`. The deadline is fixed at opening and never changes through any transition.
   - `openCase(marketId, validatorId, ground, evidenceHash)` — anyone, while `blockTimestamp < market.final.finalizedAt + CHALLENGE_WINDOW_MS`, against any validator named in that market's `reporters`, if no case for the pair exists. `ground` is exactly `"MISREPORT"` or `"NON_REPORTING"` and MUST match the market's settlement path: `MISREPORT` only when `finalizedBy == "QUORUM"` and the accused validator appears in the recorded `final.reporters` (its signature was used in the finalization); `NON_REPORTING` only when `finalizedBy == "RECOVERY"` (the reporting deadline `recoveryTime` passed without a result) and the accused validator is named in the market's `reporters`. A quorum-finalized market cannot ground a non-reporting case, and a recovered market cannot ground a misreport case. Debits `CASE_BOND` from the caller into `caseBonds`, stores the case as `OPEN`, increments `reporterOpenCases/<vid>`.
   - `addEvidence(caseId, evidenceHash)` — the opener only, while the case is `OPEN` and `blockTimestamp < deadline`; at most 8 evidence entries per case; never extends the deadline; moves no funds.
   - `resolveCase(caseId, decision)` — governance (item 8), while the case is `OPEN` and `blockTimestamp < deadline`. `decision = "DISMISSED"`: bond credited to the accused validator's registered reward address, hold released (`reporterOpenCases/<vid> −1`), status `DISMISSED`. `decision = "UPHELD"`: status `UPHELD_PENDING_PENALTY`; nothing is transferred and both the bond and the hold are retained.
   - `closeCase(caseId)` — anyone, at any time while the case is `UPHELD_PENDING_PENALTY`, if the validator protocol's penalty record for this `caseId` exists, identifying `caseId`, `validatorId`, the amount deducted and `recordedAt`, with `recordedAt < deadline`. Bond refunded to the opener, hold released, status `CLOSED`.
   - `timeoutCase(caseId)` — anyone, when `blockTimestamp ≥ deadline`, the case is `OPEN` or `UPHELD_PENDING_PENALTY`, and no penalty record with `recordedAt < deadline` exists. Bond refunded to the opener, hold released, status `TIMED_OUT`. No penalty is imposed on timeout.
   - A penalty for a `caseId` is valid for closing the case only if it was recorded while the case was `UPHELD_PENDING_PENALTY` and before `deadline`, and only one penalty record per `caseId` is recognized. How the validator protocol executes and records penalties is defined there.
   - Each of these calls updates funds, case status and `reporterOpenCases/<vid>` atomically and exactly once; the state transitions above are the only ones permitted. A case's hold does not end by itself: it becomes permissionlessly releasable at the deadline (or earlier once the penalty is recorded) through `closeCase` or `timeoutCase`. Other open markets or cases may still prevent withdrawal.
8. **Governance authorization.** `betting_v1` has a one-time `initialize(authorizedSigners, thresholdM)` callable only by the deployer and only while the governance policy is unset. `resolveCase` requires the governance authorization defined by the shared governance multisig convention, using that policy; the authorized action is the specific `resolveCase` call with its `caseId` and `decision`.

---

## 6. Contract Interface and Storage

### 6.1 Contract and Methods

One system contract, `betting_v1`, deployed from `node/smart-contracts/betting/betting_v1.ts` and registered in the allowed contract files. Methods follow the shared contract execution standard and use the engine-provided `blockTimestamp` for every time comparison. Every call is a `CONTRACT_CALL` in SBRIT with `tx.amount = '0'`; a call with a non-zero `tx.amount` MUST be rejected, and stakes move only through balance changes computed from `params`.

| Method | Caller | `params` | Effect |
|---|---|---|---|
| `initialize` | deployer, once | `{ authorizedSigners, thresholdM }` | Stores governance policy (5.4 item 8) |
| `createMarket` | anyone | market terms (3.1) | Validates terms and reporters, stores `market/<marketId>` as `OPEN` |
| `trade` | taker | `{ marketId, amount, expiry, orders[] }` | Fills orders in sequence (4.2); returns per-order statuses and fills |
| `matchOrders` | arbitrageur | `{ marketId, leftOrder, rightOrders[] }` | Atomic cross-fill (4.5) |
| `cancelGroup` | maker | `{ orderGroup }` | Stores `cancel/<tx.from>/<orderGroup> = 1` |
| `cancelAll` | maker | `{}` | Increments `cancelnonce/<tx.from>` (4.4) |
| `finalize` | anyone | `{ marketId, outcome, reports[] }` | Quorum check, stores final record (5.1) |
| `claim` | anyone | `{ marketId, addresses[] }` | Pays position holders only (5.2) |
| `recover` | anyone | `{ marketId }` | Finalizes at `cancelPrice` after `recoveryTime` (5.3) |
| `openCase` | anyone | `{ marketId, validatorId, ground, evidenceHash }` | Opens a reporter case, locks `CASE_BOND` (5.4 item 7) |
| `addEvidence` | case opener | `{ caseId, evidenceHash }` | Appends evidence to an `OPEN` case (5.4 item 7) |
| `resolveCase` | governance | `{ caseId, decision }` | `DISMISSED` or `UPHELD_PENDING_PENALTY` (5.4 item 7) |
| `closeCase` | anyone | `{ caseId }` | Closes an upheld case once the penalty is recorded (5.4 item 7) |
| `timeoutCase` | anyone | `{ caseId }` | Times out a case at its deadline (5.4 item 7) |

`trade` and `matchOrders` return in `result` one `{ inputIndex, orderHash, status, filled }` per input order, and `claim` one `{ address, status, net }` per input address, as defined in Section 4.2.1, so that applications can reconcile their books from the transaction record.

### 6.2 Storage Layout

All state lives under the contract's own namespace `sc/<cid>/kv/`:

| Key | Value |
|---|---|
| `market/<marketId>` | `{ terms, status, final?: { outcome, finalPrice, finalizedAt, finalizedBy, reporters, feeWaived, fee_total }, createdAt (blockTimestamp), createdBy }` |
| `position/<marketId>/<address>` | signed decimal string; deleted when zero |
| `collateral/<marketId>` | SBRIT held for the market: Σ positive positions while `OPEN`, unpaid remainder after finalization (4.3) |
| `openPositions/<marketId>` | count of non-zero positions, integer (4.3) |
| `reporterOpenMarkets/<vid>` | number of `OPEN` markets naming the validator as reporter, integer (5.4) |
| `reporterLastFinalizedAt/<vid>` | `blockTimestamp` of the last finalization or recovery of a market naming the validator (5.4) |
| `reporterOpenCases/<vid>` | number of unresolved reporter cases against the validator, integer (5.4) |
| `case/<caseId>` | `{ marketId, validatorId, ground, opener, openedAt, deadline, evidence[], status }` (5.4) |
| `caseBonds` | total SBRIT locked as case bonds, decimal string (5.4) |
| `policy/authorizedSigners`, `policy/thresholdM` | governance policy set by `initialize` (5.4 item 8) |
| `fill/<fillHash>` | cumulative maker amount at risk consumed |
| `cancel/<maker>/<orderGroup>` | `1` |
| `cancelnonce/<maker>` | integer as decimal string, default 0, maximum `MAX_NONCE` (4.4) |
| `dust` | cumulative collateral swept from fully settled markets, decimal string |
| `surplus` | SBRIT received outside the protocol's flows (1.4), decimal string; never used to cover obligations |

The contract writes no keys outside `sc/<cid>/kv/`. It reads validator status, signing keys, reward addresses and penalty records through the validator protocol's read interface to check reporter eligibility, pay fees and close cases. Market discovery is an application concern: applications index `createMarket` transactions from blocks; the contract maintains no market list.

### 6.3 Accounting Summary

| Movement | Debit | Credit |
|---|---|---|
| Opening trade | each party's SBRIT account | contract |
| Closing trade | contract | closing party's SBRIT account |
| Finalization | market collateral | each recorded reporter's registered reward address (`fee_total / n`, remainder to the first) |
| Claim | market collateral | position holder (`net`) |
| Rounding remainder | market collateral | `dust`, after the market's last position is cleared |
| Case opened | opener's SBRIT account | `caseBonds` |
| Case dismissed | `caseBonds` | accused validator's registered reward address |
| Case closed or timed out | `caseBonds` | opener's SBRIT account |

Disposition of accumulated `dust` and `surplus` is outside this version of the protocol; both MUST remain in the contract.

---

## 7. Implementation Notes

### 7.1 Execution Dependencies

The betting contract follows the shared Streembit contract execution standard. All betting time comparisons — order `expiry`, trade `expiry`, `acceptanceCutoff`, `recoveryTime`, `finalizedAt`, `createdAt`, case deadlines and the challenge window — use the engine-provided `blockTimestamp`. Orders in `params` are processed in the supplied order.

### 7.2 Encoding

Structured data and hashes follow Section 2.2 and the shared canonical-form conventions. Signed messages (orders, reports) carry the fields listed in their schemas in Sections 4.1 and 5.1.

### 7.3 Validator Records

The betting contract reads validator status, signing keys, reward addresses and penalty records through the validator protocol's read interface, and exposes `reporterOpenMarkets/<vid>`, `reporterLastFinalizedAt/<vid>` and `reporterOpenCases/<vid>` for the validator protocol to read. The validator protocol is responsible for honouring outstanding reporting obligations before deposit release and for executing and recording penalties (Section 5.4).

### 7.4 Betting Implementation Requirements

1. **Contract registration.** Register `betting_v1` in the allowed contract files.
2. **Governance policy.** `initialize` is performed once after deployment under the shared governance convention (Section 5.4 item 8).
3. **Validator protocol.** Deposit-release checks against this contract's reporter records and penalty execution keyed by `caseId` are required of the validator protocol before cases can be closed.

Canonical encoding, contract execution, address derivation and storage-key encoding are shared dependencies; their implementation and verification belong in the corresponding specifications and engineering backlog, not in this protocol.

### 7.5 Security Notes

Misreporting is bounded by the reporter quorum, the validators' locked deposits and the case process of Section 5.4; a finalization is never reversed, so a wrong result stands and the remedy is the penalty on the reporter. Orders cannot be refilled beyond their allowance because fills are counted against `fillHash`. `recoveryTime` MUST be set late enough that a legitimate report is not pre-empted by a `recover` call, and early enough that funds are not locked indefinitely.

## References

Degens Protocol — architectural reference for standing orders, partial fills, tradable positions, collateral, result reporting and recovery. The Streembit Betting Protocol is an independent design adapted to Streembit.

Protocol documentation: https://degensprotocol.github.io/degens-contract/protocol.html

Source reference: https://github.com/degensprotocol/degens-contract/tree/0bb15c070f3046409b5a25588978b9819ad3d003
