Skip to content

Slashing vulnerabilities in Krunk and SpacePoker #413

Description

@trgarrett

Since these are pre-release modules and not listed in the HackerOne scope, I am submitting issues here.

Krunk clue.clsp — Unslashable Premature Reveal (CRITICAL)

In chia-gaming's krunk (Wordle-style) game, the on-chain validator at clue.clsp:70-84 checks whether Alice's word reveal is valid using an if_any_fail block with three conditions. Condition 1 (line 71) checks strlen(MOVE) == 21, condition 2 (line 72) verifies the sha256 commitment binding, and condition 3 (lines 73-80) compares MOVER_SHARE against the expected payout. Inside condition 3, if Bob hasn't guessed the word correctly AND hasn't exhausted all 5 guesses, the code reaches (x "clue reveal before terminal") at line 78 — a CLVM raise intended as a "should be unreachable" assertion. Because the and macro (and.clinc) short-circuits left-to-right, conditions 1 and 2 both evaluate to TRUE for a legitimate premature reveal (correct length, correct commitment), so condition 3 IS evaluated. The raise fires during condition 3's computation, before any comparison can return false, aborting the entire slash spend. Per the referee's security model (referee.clsp:68), a validator that raises makes the cheat permanently unslashable.

The attack sequence:

Alice commits sha256(salt, word) for a valid dictionary word via commit.clsp
Bob makes his first guess (wrong) — state transitions to clue.clsp with bob_guesses = (G1), alice_clues = ()
Alice goes on-chain and submits MOVE = salt||word (21 bytes) via the referee's move path with MOVER_SHARE=0, infohash_c=0 (terminal) — the referee accepts optimistically without validation (referee.clsp:97-108)
Bob attempts to slash with evidence = nil — validator reaches (x "clue reveal before terminal") → raise → slash spend invalid
Bob attempts to slash with 10-byte evidence (dictionary gap) — Alice used a valid dictionary word, so no gap range contains it → unavailable
Bob attempts to slash with 1-byte evidence (wrong-clue index) — alice_clues is empty, so (< evidence (len alice_clues)) at line 64 fails → assert crashes
Bob cannot move — INFOHASH_B=0 blocks the referee's move branch at referee.clsp:99
After timeout, Bob (MOVER) gets MOVER_SHARE=0, Alice (WAITER) collects AMOUNT - 0 = 100% of the pot

Spacepoker end.clsp — Out-of-Range Bitfield Showdown Theft (CRITICAL)

In spacepoker's terminal validator at end.clsp, each player selects 5 of their 7 cards (2 hole + 5 community, built at line 92) using a 1-byte bitfield. The only validation is popcount(mover_bitfield) == 5 at line 31. However, only bits 0–6 correspond to actual cards. The pull_from_bitfield function (lines 61-70) walks the 7-card list and terminates when the list is exhausted — bit 7 (0x80) is silently ignored. A bitfield like 0x8F (bits 0,1,2,3,7) has popcount=5 but selects only 4 cards. When space_hand_eval (space_hand_eval.clinc:39-57) receives 4 distinct-rank cards, group_by_count produces 4 groups, yielding a 9-element hand list. The straight-detection branch at line 49 destructures with (_ _ _ _ _ _ r1 _ _ _ r5) — an 11-position pattern. Accessing position 10 of a 9-element list hits (f nil), which raises "first of non-cons". This raise aborts the slash spend, making the cheat unslashable.

The attack sequence:

The game proceeds normally through betting rounds to the river (N=1)
A call in mid_round.clsp transitions to end.clsp with max_move_size=17
The final revealer (MOVER) submits MOVE = preimage(16 bytes) || bitfield(1 byte) where bitfield = 0x80 | (4 in-range bits) — e.g. 0x8F, which has popcount=5
The referee's move path accepts it: strlen(17) <= MAX_MOVE_SIZE(17), MOVER_SHARE=0 in [0, AMOUNT], infohash_c=0 (terminal) — all pass (referee.clsp:97-108)
Opponent attempts to slash — pull_from_bitfield returns only 4 cards → space_hand_eval builds a 9-element list → 11-position destructure at space_hand_eval.clinc:49 → raise → slash spend invalid
Opponent cannot move — INFOHASH_B=0 blocks further moves
After timeout, the attacker (WAITER after role swap) collects AMOUNT - 0 = 100% of the pot regardless of who actually held the winning hand

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions