Skip to content

[coin_manager] Missing overflow protection on account_balance_up / contract_balance_up #10

@GideonBature

Description

@GideonBature

Summary

The "increase balance" operations in CoinManager compute the new balance with a plain +, which silently wraps on overflow for u64. The corresponding _down paths correctly guard against going below zero, but the _up paths have no upper-bound/overflow check. For a component that stores Bitcoin-denominated balances, this is a correctness bug — a malicious or buggy caller supplying a very large up_value can wrap an account/contract balance to a small value.

Affected file(s)

  • src/inscriptive/coin_manager/coin_manager.rs

Location

Function Line Expression
account_balance_up ~822 let new_account_balance_in_satoshis: u64 = account_balance_in_satoshis + up_value_in_satoshis;
contract_balance_up ~883 let new_contract_balance_in_satoshis: u64 = existing_contract_balance_in_satoshis + up_value_in_satoshis;

For reference, the _down variants (account_balance_down ~836, contract_balance_down ~896) do guard the lower bound (e.g. AccountBalanceWouldGoBelowZero, ContractBalanceWouldGoBelowZero), which makes the asymmetry on the upper bound stand out.

Root cause / analysis

Both _up functions follow the same shape:

pub fn account_balance_up(
    &mut self,
    account_key: AccountKey,
    up_value_in_satoshis: u64,
) -> Result<(), CMAccountBalanceUpError> {
    let account_balance_in_satoshis: u64 = self.get_account_balance(account_key).ok_or(...)?;
    let new_account_balance_in_satoshis: u64 =
        account_balance_in_satoshis + up_value_in_satoshis;   // <-- wraps on overflow
    self.delta.ephemerally_update_account_balance(account_key, new_account_balance_in_satoshis);
    Ok(())
}

u64::MAX is 18_446_744_073_709_551_615. A caller passing up_value_in_satoshis = u64::MAX against a non-zero existing balance wraps to a value below the existing balance — and because there is no check that the new balance is >= the old balance, the wrap is accepted silently and persisted by apply_changes.

The deferred-proportional path in the same file already uses checked_add(...).expect(...) for its own overflow (e.g. ~lines 544-553), so overflow is being considered elsewhere in this module — it was just not applied on the primary balance-increase entry points.

Impact

  • Silent corruption of account/contract balances in the database.
  • Possible under-collateralization of shadow spaces: allocs_sum ≤ balance is enforced on shadow_up/contract_balance_down, but a wrapped (small) balance could make a previously-valid allocation set suddenly exceed the balance, or conversely leave the invariant satisfiable when it should not be.
  • Low likelihood under normal operation, but for a value store the failure mode (silent wrap vs. rejected call) should always be rejection.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions