- Event — an on-chain record of a concert, flight, sports match,
etc. Created via
create_event; owns a resale policy. - Ticket — an on-chain asset tied to one
Event, owned by exactly oneAddressat a time. - Tier — a free-text label on a ticket (e.g. "GA", "VIP") set at issuance; not separately validated against the event.
- Check-in — the one-way
Valid -> Usedtransition performed at the point of entry. - Resale cap —
max_resale_multiplier_bpson the event; the ceiling a ticket can be relisted for, relative to its original price. Must be at least 10,000 bps (face value): a lower multiplier would cap every resale below face value and make resale impossible, so it is rejected withInvalidMultiplierat creation. - Royalty —
royalty_bpson the event; the organizer's cut of every resale, paid atomically with the ownership transfer.
Every ticket transitions through a state machine with four possible states: Valid, Used, Revoked, and Resale. This section documents valid transitions and the semantics of each state.
Valid — The ticket has been issued or transferred and is ready to use. The owner may:
- Transfer it to another address (
transfer_ticket) - Check it in at entry (
check_in, one-way toUsed) - List it for resale (
list_for_resale, transition toResale) - Have it revoked by the organizer (
revoke_ticket, one-way toRevoked)
Used — The ticket has been checked in and the attendee has entered the event. This is a terminal state for attendance — no further operations are possible on a used ticket.
- Cannot be transferred, resold, or revoked
- Prevents any state-changing operation (enforced by
AlreadyUsederror) - Read-only lookup via
verify_ticketis still allowed
Revoked — The ticket has been permanently voided by the organizer (e.g., due to chargeback, policy violation, or refund). This is a terminal state.
- Cannot be transferred, resold, checked in, or transferred again
- Prevents any state-changing operation (enforced by
Revokederror) - Read-only lookup via
verify_ticketis still allowed - If a refund was issued via
revoke_with_refund, the original price is returned to the ticket owner
Resale — The ticket is actively listed for resale under the event's resale cap. The owner may:
- Cancel the listing (
cancel_resale, return toValid) - Keep the listing (no state change until buyer acts)
- Cannot check in or transfer while in this state
When a buyer purchases a resale listing via buy_resale:
- The organizer's royalty is paid (in the payment token)
- The remaining proceeds are paid to the original owner
- Ownership is transferred to the buyer (state remains
Validfor the new owner)
┌─────────────────────────────────────────┐
│ │
┌─────▼─────┐ │
│ Valid │ │
└─────┬─────┘ │
│ │
┌──────────┼──────────┬──────────────────┐ │
│ │ │ │ │
transfer() check_in() revoke_ticket() list_for_resale()│
│ │ │ │ │
▼ ▼ ▼ ▼ │
┌─────────┐ ┌──────┐ ┌─────────┐ ┌───────────┐ │
│ Valid │ │ Used │ │ Revoked │ │ Resale │ │
│(new │ │ │ │ │ │ │ │
│ owner) │ │(end) │ │ (end) │ └─────┬─────┘ │
└─────────┘ └──────┘ └─────────┘ │ │
cancel_resale() │
│ │
└───────────┘
Primary sale and check-in:
- Organizer calls
issue_ticket(to=Alice, ...)→ Ticket entersValidstate owned by Alice - Alice checks in at the gate → Organizer calls
check_in(...)→ Ticket entersUsedstate - Alice is now admitted; ticket is marked used and cannot be transferred or resold
Transfer and resale:
- Organizer calls
issue_ticket(to=Alice, price=100)→ Ticket entersValidstate - Alice transfers to Bob → Ticket remains
Valid, now owned by Bob - Bob lists for resale at 120 → Ticket enters
Resalestate - Carol buys the resale → Organizer gets royalty (5% = 5), Bob gets 115, Carol owns ticket in
Validstate - Carol checks in → Ticket enters
Usedstate
Revocation with refund:
- Organizer issues ticket to Alice for $100
- Chargeback occurs → Organizer calls
revoke_with_refund(ticket_id, refund=100)→ Ticket entersRevokedstate, Alice receives $100 refund - Ticket can never be used, transferred, or resold again
Invalid transitions (all fail with specific errors):
transfer_ticketon aUsedticket →AlreadyUsedcheck_inon aRevokedticket →Revokedbuy_resaleon a ticket not inResalestate →NotForResalerevoke_ticketon aUsedticket → succeeds, but ticket is nowRevoked
| From State | Operation | To State | Error if fails |
|---|---|---|---|
Valid |
transfer_ticket(...) |
Valid (new owner) |
NotOwner |
Valid |
check_in(...) |
Used |
NotOrganizer |
Valid |
revoke_ticket(...) |
Revoked |
NotOrganizer |
Valid |
list_for_resale(...) |
Resale |
ResalePriceExceedsCap |
Used |
any state-changing op | — | AlreadyUsed |
Revoked |
any state-changing op | — | Revoked |
Resale |
cancel_resale(...) |
Valid |
NotOwner |
Resale |
buy_resale(...) |
Valid (new owner) |
NotOwner (of payment token balance) |
Resale |
transfer_ticket(...) |
— | (blocked; must cancel first) |
Per issue #206, every state-changing entry point checks authorization (require_auth()) before validating state. This ordering ensures that an unauthenticated caller who invokes revoke_ticket on a non-existent ticket gets an auth error, not TicketNotFound — preventing information leakage about which tickets exist.
See contracts/ticketing/src/lib.rs and the require_auth_runs_before_business_validation test in contracts/ticketing/src/test/auth.rs for details.