Fix money-safety bugs in economy (heist, blackjack, bail) #3

Merged
renkar merged 1 commits from fix/economy-money-safety into master 2026-08-10 15:35:46 +00:00
Owner

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

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>
renkar added 1 commit 2026-08-10 15:35:40 +00:00
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>
renkar merged commit 5e303fe36f into master 2026-08-10 15:35:46 +00:00
Sign in to join this conversation.
No Reviewers
No Label
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: renkar/tipibot#3