How It Works¶
This page walks through the full on-chain lifecycle of a wager, from creation to payout, including every exit path.
See also: Roles and Tiers for the membership / admin-role model, and the Account Moderation Policy for the per-account freeze power.
Lifecycle state machine¶
stateDiagram-v2
[*] --> Open: createWager()<br/>creator stake escrowed
Open --> Active: acceptWager()<br/>opponent stake escrowed
Open --> Refunded: cancelOpen() / declineWager() /<br/>acceptance deadline passes
Active --> Resolved: declareWinner() or<br/>autoResolveFromPolymarket() /<br/>autoResolveFromOracle()
Active --> Draw: declareDraw()<br/>(both parties consent,<br/>or arbitrator ruling)
Active --> Refunded: claimRefund()<br/>after resolve deadline
Resolved --> [*]: claimPayout()<br/>winner takes full pot
Draw --> [*]: stakes returned<br/>automatically
Refunded --> [*]
1. Creation (Open)¶
The creator calls WagerRegistry.createWager() (or createWagerWithTerms(),
which additionally binds the current terms-of-service hash on-chain) with:
- the opponent address (and optionally an arbitrator for third-party resolution),
- the stake token (USDC by default) and both stake amounts — equal stakes for an even-money wager, asymmetric stakes for an Offer (odds) wager where the settler puts up the majority stake,
- an acceptance deadline and a resolve deadline,
- the resolution type (see below) plus an oracle condition ID if oracle-resolved,
- a metadata hash/URI pointing at the (optionally encrypted) terms on IPFS.
The creator's stake transfers into escrow immediately and WagerCreated is
emitted. Before any state changes, the registry checks:
- Membership —
MembershipManager.checkCanCreate()enforces the caller's tier limits (monthly creations and concurrent open wagers). - Sanctions —
SanctionsGuard.checkBlocked()reverts if the creator is on the Chainalysis sanctions list or the operator deny list.
2. Acceptance (Open → Active)¶
The opponent — usually arriving via a QR code or deep link — calls
acceptWager(). Their stake transfers into escrow, both parties are screened
by SanctionsGuard, and WagerAccepted is emitted. The wager is now live
until its end time.
If the opponent never accepts:
- the creator can
cancelOpen()at any time, or - after the acceptance deadline anyone can call
claimRefund()orbatchExpireOpen()to return the creator's stake.
The opponent can also explicitly declineWager(), which refunds the creator
immediately.
3. Resolution (Active → Resolved / Draw)¶
How a wager resolves is fixed at creation time:
| Resolution type | Who can settle | How |
|---|---|---|
Either |
Creator or opponent | declareWinner(wagerId, winner) |
Creator |
Creator only | declareWinner(...) |
Opponent |
Opponent only | declareWinner(...) |
ThirdParty |
Named arbitrator | declareWinner(...) |
Polymarket |
Anyone (permissionless trigger) | autoResolveFromPolymarket(wagerId) reads the linked Polymarket CTF condition |
ChainlinkDataFeed |
Anyone | autoResolveFromOracle(wagerId) compares a Chainlink price feed against the registered threshold |
ChainlinkFunctions |
Anyone | autoResolveFromOracle(wagerId) reads the fulfilled Chainlink Functions request |
UMA |
Anyone | autoResolveFromOracle(wagerId) reads the settled UMA Optimistic Oracle V3 assertion |
The on-chain enum names are unchanged. In the create UI these are surfaced as
Me (Creator), Them (Opponent), A Friend (ThirdParty), and
An Oracle (Polymarket / Chainlink / UMA). Either is retained on-chain for
pre-existing wagers but is no longer offered when creating new ones — every new
wager names a single settler, which in an Offer also carries the majority stake.
For oracle types, the creator declares at creation which side they take
(creatorIsYes); when the adapter reports the outcome, the registry maps it to
a winner and emits WagerResolved. All four adapters implement the same
IOracleAdapter interface (isConditionResolved / getOutcome), so the
registry treats them uniformly.
Draws¶
Participant-resolved wagers can settle as a draw via declareDraw(): the first
call records one party's consent (DrawProposed), the second call from the
other party settles it (WagerDrawn) and returns each side's own stake. For
third-party wagers the arbitrator's single declareDraw() settles immediately.
A consenting party can back out with revokeDraw() before the other side
agrees.
4. Settlement¶
- Win — the winner calls
claimPayout(wagerId)and receives the full pot (both stakes). Payouts are pull-based and can only be claimed once (PayoutClaimed). - Draw — stakes are returned to each party during settlement; no claim step is needed.
- Timeout — if an
Activewager passes its resolve deadline unresolved, anyone can callclaimRefund(wagerId); both stakes go back to their owners (WagerRefunded). This is the safety net for oracles that never report and counterparties that disappear.
End-to-end sequence¶
sequenceDiagram
actor C as Creator
actor O as Opponent
participant FE as FairWins SPA
participant WR as WagerRegistry
participant MM as MembershipManager
participant SG as SanctionsGuard
participant IPFS as IPFS
C->>FE: fill wager form
FE->>IPFS: upload encrypted terms (optional)
FE->>WR: createWager(opponent, stakes, deadlines, type, metadataUri)
WR->>SG: checkBlocked(creator)
WR->>MM: checkCanCreate / recordCreate
WR-->>C: WagerCreated (stake escrowed)
C-->>O: QR code / deep link
O->>FE: open /friend-market/accept?marketId=N
FE->>WR: acceptWager(N)
WR->>SG: checkBlocked(opponent, creator)
WR-->>O: WagerAccepted (stake escrowed)
Note over WR: wager Active until end time
C->>WR: declareWinner(N, winner) ⟂ or oracle auto-resolve
WR-->>C: WagerResolved
O->>WR: claimPayout(N)
WR->>O: full pot transferred
Memberships and limits¶
Wager participation requires an active membership purchased through
MembershipManager (purchaseTier() / purchaseTierWithTerms()), priced in
USDC. Each tier — Bronze, Silver, Gold, Platinum — sets a monthly creation
allowance and a cap on concurrently open wagers. The registry calls
recordCreate / recordClose hooks so the limits track actual usage. Admins
can also grant or revoke memberships out of band. Full details:
Roles and Tiers.
What keeps it honest¶
| Property | Mechanism |
|---|---|
| Loser can't dodge payment | both stakes escrowed at acceptance |
| Funds can't get stuck | refund paths after every deadline; batchExpireOpen for stale offers |
| Outcome can't be forged | resolution authority fixed at creation; oracle adapters read external sources |
| Terms can't be rewritten | metadata hash recorded on-chain at creation |
| Sanctioned use blocked | SanctionsGuard screening on create and accept |
| Operator overreach bounded | Guardian can pause, Moderator can freeze accounts — neither can move escrowed funds |