Veterancy ranks — promotion is earned and can never be rolled back
Competitive games have shipped both halves of this bug: rank-rollback (disconnect during the promotion screen and log back in demoted — Halo and several ladder games paid out compensation waves for it) and rank-injection (a modified client reports Grandmaster and the server believes it). Both are writes the server should never accept. Here XP is walled into 0..100000 — no death-penalty underflow, no injected overflow — and rank is a FORWARD-ONLY machine: novice -> adept -> master -> grandmaster. soma verify proves there are no demotion edges and that grandmaster is terminal: a rank, once earned, is permanent.
A Soma program from the games_economy part of the verified corpus: 92 lines, cells Veterancy, RankTests. It passes soma check with soma 3.2.4.
How it is built
The cell Veterancy declares 4 signals in its face: earn_xp(player: String, xp_amt: Int) -> Map, claim_promotion(player: String) -> Map, xp_of(player: String) -> Int and rank_of(player: String) -> String; keeps 1 memory slot: xp (Map<String, Int>, persistent and consistent) under the invariant xp >= 0 && xp <= 100000. Its state machine rank starts in novice and allows 3 transitions: novice -> adept, adept -> master and master -> grandmaster. grandmaster is terminal. Handlers: earn_xp (line 35), claim_promotion (line 42), xp_of (line 65) and rank_of (line 66).
The file also holds 1 test cell (RankTests) with 15 assertions, run by soma test.
The source
veterancy_ranks.cell
// ============================================================================
// VETERANCY RANKS — promotion is earned and can never be rolled back
// ============================================================================
// Competitive games have shipped both halves of this bug: rank-rollback
// (disconnect during the promotion screen and log back in demoted — Halo
// and several ladder games paid out compensation waves for it) and
// rank-injection (a modified client reports Grandmaster and the server
// believes it). Both are writes the server should never accept. Here XP is
// walled into 0..100000 — no death-penalty underflow, no injected overflow —
// and rank is a FORWARD-ONLY machine: novice -> adept -> master ->
// grandmaster. soma verify proves there are no demotion edges and that
// grandmaster is terminal: a rank, once earned, is permanent.
// ============================================================================
cell Veterancy {
face {
signal earn_xp(player: String, xp_amt: Int) -> Map
signal claim_promotion(player: String) -> Map
signal xp_of(player: String) -> Int
signal rank_of(player: String) -> String
}
memory {
xp: Map<String, Int> [persistent, consistent]
invariant xp >= 0 && xp <= 100000
}
state rank {
initial: novice
novice -> adept
adept -> master
master -> grandmaster
}
on earn_xp(player: String, xp_amt: Int) {
if xp_amt <= 0 { return map("ok", false, "reason", "xp must be positive") }
let w = try { xp.set(player, (xp.get(player) ?? 0) + xp_amt) }
if w.error != () { return map("ok", false, "reason", "xp out of range") }
return map("ok", true, "xp", xp.get(player))
}
on claim_promotion(player: String) {
let cur = rank_of(player)
let have = xp_of(player)
let next = ""
let need = 0
if cur == "novice" {
next = "adept"
need = 1000
}
if cur == "adept" {
next = "master"
need = 10000
}
if cur == "master" {
next = "grandmaster"
need = 50000
}
if next == "" { return map("ok", false, "reason", "{player} is already grandmaster") }
if have < need { return map("ok", false, "reason", "need {need} xp, have {have}") }
transition(player, next)
return map("ok", true, "rank", next)
}
on xp_of(player: String) { return xp.get(player) ?? 0 }
on rank_of(player: String) { return get_status(player) }
}
cell test RankTests {
rules {
assert earn_xp("li", 1200).ok == true
assert claim_promotion("li").ok == true
assert rank_of("li") == "adept"
// not enough XP for master — and a client cannot inject a rank skip
assert claim_promotion("li").ok == false
assert_fails transition("li", "grandmaster")
// NO DEMOTION EDGE EXISTS — the rollback bug is unrepresentable
assert_fails transition("li", "novice")
assert earn_xp("li", 50000).ok == true
assert claim_promotion("li").ok == true
assert claim_promotion("li").ok == true
assert rank_of("li") == "grandmaster"
// grandmaster is terminal: nothing left to claim, nothing to lose
assert claim_promotion("li").ok == false
assert_fails transition("li", "master")
// XP cannot underflow on death penalties or overflow on injection
assert earn_xp("li", -500).ok == false
assert_fails xp.set("li", -1)
assert_fails xp.set("li", 100001)
}
}What the toolchain says
$ soma check veterancy_ranks.cell
✓ All checks passed.$ soma test veterancy_ranks.cell
test RankTests ...
✓ assert earn_xp("li", 1200).ok == true
✓ assert claim_promotion("li").ok == true
✓ assert rank_of("li") == "adept"
✓ assert claim_promotion("li").ok == false
✓ assert_fails transition("li", "grandmaster") — raised invalid transition: adept → grandmaster. Current state: 'adept'. Valid targets: [master]
✓ assert_fails transition("li", "novice") — raised invalid transition: adept → novice. Current state: 'adept'. Valid targets: [master]
✓ assert earn_xp("li", 50000).ok == true
✓ assert claim_promotion("li").ok == true
✓ assert claim_promotion("li").ok == true
✓ assert rank_of("li") == "grandmaster"
✓ assert claim_promotion("li").ok == false
✓ assert_fails transition("li", "master") — raised invalid transition: grandmaster → master. Current state: 'grandmaster'. Valid targets: []
✓ assert earn_xp("li", -500).ok == false
✓ assert_fails xp.set("li", -1) — raised memory invariant violated on 'xp': xp >= 0 && xp <= 100000 — rejected write of -1 (key "li"); the slot is unchanged
✓ assert_fails xp.set("li", 100001) — raised memory invariant violated on 'xp': xp >= 0 && xp <= 100000 — rejected write of 100001 (key "li"); the slot is unchanged
15 tests: 15 passed, 0 failed$ soma verify veterancy_ranks.cell
Verifying veterancy_ranks.cell...
State machine 'rank': 4 states, initial 'novice'
States: [adept, grandmaster, master, novice]
✓ 4 states, 3 transitions
✓ all states reachable from 'novice'
✓ terminal states: [grandmaster]
✓ no deadlocks
✓ liveness: every state can eventually reach a terminal state
⚠ dynamic transition targets: handler `claim_promotion` uses a dynamic (computed) transition target — the state these handlers move to is computed at run time (from input or an LLM answer), so the machine's safety is checked at run time for them, not proven
✓ termination: all 4 handlers structurally terminate
⚠ refinement (partial — dynamic targets fall back to runtime check): handler `claim_promotion` calls transition() with a computed target — which edge it takes is checked at run time (invalid_transition), not proven; write the target as a literal state to have it verified
✓ refinement (partial — dynamic targets fall back to runtime check): handler `claim_promotion` ⟶ <dynamic>
Veterancy/invariants:
✓ invariant xp >= 0 — writer 'earn_xp' proven by induction (writes (xp.get(player) ?? 0) + xp_amt)
⚠ invariant xp >= 0 && xp <= 100000 — runtime-checked (computed values): earn_xp → xp [xp <= 100000] because `xp` has no known range
8 passed, 3 warnings, 0 failures
Temporal properties for 'rank':
✓ deadlock_free — no deadlocks in any reachable state
Temporal: 1 passed, 0 failed
VERIFY OK