Fix money-safety bugs in economy (heist, blackjack, bail)
Multi-agent audit of the money-moving economy modules surfaced three confirmed correctness bugs. All three are fixed here with regression tests. heist (core/economy/heist.py): - `house = await get_user(house.HOUSE_ID)` shadowed the imported `house` module for the whole function, so `house.HOUSE_ID` raised UnboundLocalError on every successful heist (win payout was entirely dead) and on the fail-path compensation branch. Rename the local to `house_rec`. - Un-shadowing exposed a latent mint: the pot was floored at 300 but the house was debited only min(total, balance), so a poor house paid out more than it lost. Cap the pot at the balance and debit exactly what is paid (house debit == sum of payouts). No mint, no leak. - Add the missing `_is_jailed` import (do_heist_check referenced it unimported). blackjack (commands/economy_games_commands.py): - Button callbacks had no reentrancy guard; discord.py dispatches each click as its own task, so double-clicking Stand within the dealer-reveal window paid out twice (mint), and double-clicking Double/Split deducted the extra bet twice. Add a synchronous `_busy` guard (matching the existing RpsGame idiom) on all four callbacks plus a `_resolved` idempotency flag on settlement, so a game can only pay out once. bail (core/economy/jail.py, commands/economy_extra_commands.py): - do_bail only checked balance, never jail state; a double-click or a stale BailView from a re-run /jailbreak charged bail twice, destroying coins (bail is a pure sink). Make do_bail a no-op when the user is not jailed, and add a UI reentrancy guard + "already free" message. Tests: 47 passed (4 new regression tests covering heist coin-conservation and bail idempotency). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -50,6 +50,12 @@ async def do_bail(user_id: int) -> dict:
|
||||
"""Charge bail fine after exhausting jailbreak rolls and free the user.
|
||||
Fine = 20-30% of current balance, floored at 350. If balance < 350, stay jailed."""
|
||||
user = await get_user(user_id)
|
||||
# Idempotency guard: only an actively-jailed user can be charged bail. The
|
||||
# first successful call clears jailed_until, so a rapid second click or a
|
||||
# stale BailView from a re-run /jailbreak becomes a no-op instead of a
|
||||
# second fine (bail is a pure sink - a double charge destroys coins).
|
||||
if not _is_jailed(user):
|
||||
return {"ok": False, "reason": "not_jailed", "balance": user["balance"]}
|
||||
if user["balance"] < MIN_BAIL:
|
||||
return {"ok": False, "reason": "broke", "balance": user["balance"]}
|
||||
pct = random.uniform(0.20, 0.30)
|
||||
|
||||
Reference in New Issue
Block a user