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.
Summary
The "increase balance" operations in
CoinManagercompute the new balance with a plain+, which silently wraps on overflow foru64. The corresponding_downpaths correctly guard against going below zero, but the_uppaths 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 largeup_valuecan wrap an account/contract balance to a small value.Affected file(s)
src/inscriptive/coin_manager/coin_manager.rsLocation
account_balance_uplet new_account_balance_in_satoshis: u64 = account_balance_in_satoshis + up_value_in_satoshis;contract_balance_uplet new_contract_balance_in_satoshis: u64 = existing_contract_balance_in_satoshis + up_value_in_satoshis;For reference, the
_downvariants (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
_upfunctions follow the same shape:u64::MAXis18_446_744_073_709_551_615. A caller passingup_value_in_satoshis = u64::MAXagainst 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 byapply_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
allocs_sum ≤ balanceis enforced onshadow_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.