What changed, and why it matters
This commit fixes how Stack Wallet handles Firo masternode collateral UTXOs. Previously, the wallet could auto-freeze a UTXO, the user could deliberately unfreeze it, and then a later sync would re-freeze it automatically. The patch prevents that automatic re-freezing once a user has unfrozen a UTXO, while still allowing a first-time auto-freeze when masternode collateral is newly detected. It also improves the labels shown to users when collateral is detected with certainty versus only suspected. The pubspec.lock changes update unrelated dependencies and are not directly security-relevant.
Review the freeze/unfreeze UI code to confirm it never clears blockedReason, since the new userUnfroze heuristic depends on that invariant. Consider adding a dedicated userUnfroze boolean field instead of inferring intent from leftover metadata. Verify that the Firo masternode detection RPC failure path still blocks the UTXO as intended. The dependency updates in pubspec.lock should be reviewed separately for supply-chain relevance, but they are outside the scope of this fix.
Security signals we found
Auto-freeze logic could previously override an explicit user unfreeze, risking denial-of-spend or unexpected locked funds.
Masternode collateral UTXOs, if spent, invalidate the masternode; the patch improves labeling and preserves user intent.
The userUnfroze heuristic depends on the invariant that unfreezing only flips isBlocked and does not clear blockedReason; if that invariant is violated elsewhere, the protection collapses.
No input validation, cryptographic, or network-layer changes are present in the diff.
Evidence from the diff
The core change is in MainDB.updateUTXOs: when refreshing UTXOs, the code now preserves stored isBlocked/userUnfroze state and only applies a new auto-block if the incoming UTXO is blocked, the stored UTXO was not blocked, and the user had not previously unfrozen it. A new computed getter userUnfroze on UTXO detects a deliberate user unfreeze by checking !isBlocked && blockedReason != null, relying on the UI only toggling isBlocked and never clearing blockedReason. In FiroWallet, when a masternode registration lookup succeeds, the UTXO now gets a definitive ‘Masternode collateral’ label/reason; the fallback ‘Possible masternode collateral’ text now uses ??= to avoid overwriting that stronger label. The fix reduces the risk of accidental spend of masternode collateral and prevents the wallet from overriding an explicit user unfreeze decision.
Changed components
lib/db/isar/main_db.dartlib/models/isar/models/blockchain_data/utxo.dartlib/wallets/wallet/impl/firo_wallet.dartInspect captured patch +64 / −35
diff --git a/lib/db/isar/main_db.dart b/lib/db/isar/main_db.dart
index 3b86d74..8958114 100644
--- a/lib/db/isar/main_db.dart
+++ b/lib/db/isar/main_db.dart
@@ -333,6 +333,14 @@ class MainDB {
if (storedUtxo != null) {
// update
+ // Preserve user-set flags, but allow a fresh auto-freeze (e.g. firo
+ // masternode collateral detected after registration) unless the
+ // user deliberately unfroze this utxo before. Never auto-unfreeze:
+ // a flaky network check must not unlock coins.
+ final applyAutoBlock =
+ utxo.isBlocked &&
+ !storedUtxo.isBlocked &&
+ !storedUtxo.userUnfroze;
set.remove(utxo);
set.add(
storedUtxo.copyWith(
@@ -341,6 +349,12 @@ class MainDB {
blockTime: utxo.blockTime,
blockHeight: utxo.blockHeight,
blockHash: utxo.blockHash,
+ // passing null keeps the stored value
+ isBlocked: applyAutoBlock ? true : null,
+ blockedReason: applyAutoBlock ? utxo.blockedReason : null,
+ name: applyAutoBlock && storedUtxo.name.isEmpty
+ ? utxo.name
+ : null,
),
);
} else {
diff --git a/lib/models/isar/models/blockchain_data/utxo.dart b/lib/models/isar/models/blockchain_data/utxo.dart
index f417b4c..988a713 100644
--- a/lib/models/isar/models/blockchain_data/utxo.dart
+++ b/lib/models/isar/models/blockchain_data/utxo.dart
@@ -94,6 +94,14 @@ class UTXO {
(isCoinbase ? minimumCoinbaseConfirms : minimumConfirms);
}
+ /// A lingering [blockedReason] on an unblocked utxo means the wallet
+ /// auto-froze it previously and the user deliberately unfroze it. Used to
+ /// prevent auto re-freezing in [MainDB.updateUTXOs]. Relies on the
+ /// freeze/unfreeze toggles only flipping [isBlocked] and never clearing
+ /// [blockedReason].
+ @ignore
+ bool get userUnfroze => !isBlocked && blockedReason != null;
+
// fuzzy
bool _isMonero() {
return keyImage != null;
diff --git a/lib/wallets/wallet/impl/firo_wallet.dart b/lib/wallets/wallet/impl/firo_wallet.dart
index ff2996e..48970b5 100644
--- a/lib/wallets/wallet/impl/firo_wallet.dart
+++ b/lib/wallets/wallet/impl/firo_wallet.dart
@@ -759,6 +759,13 @@ class FiroWallet<T extends ElectrumXCurrencyInterface> extends Bip39HDWallet<T>
txid: jsonTX!["txid"] as String,
index: jsonUTXO["tx_pos"] as int,
);
+
+ if (blocked) {
+ blockedReason =
+ "Masternode collateral. "
+ "Unlocking and spending will invalidate this masternode!";
+ label = "Masternode collateral";
+ }
} catch (_) {
// call failed, lock utxo just in case
// it should logically already be blocked
@@ -768,10 +775,10 @@ class FiroWallet<T extends ElectrumXCurrencyInterface> extends Bip39HDWallet<T>
}
if (blocked) {
- blockedReason =
+ blockedReason ??=
"Possible masternode collateral. "
"Unlock and spend at your own risk.";
- label = "Possible masternode collateral";
+ label ??= "Possible masternode collateral";
}
}
diff --git a/pubspec.lock b/pubspec.lock
index 115772a..f33d8cc 100644
--- a/pubspec.lock
+++ b/pubspec.lock
@@ -96,8 +96,8 @@ packages:
dependency: "direct main"
description:
path: "."
- ref: "3ef6b94375d7b4d972b0bc0bd9597532381a88ec"
- resolved-ref: "3ef6b94375d7b4d972b0bc0bd9597532381a88ec"
+ ref: bdc0c0788d1d6dfb04863a793955f848ba1624a8
+ resolved-ref: bdc0c0788d1d6dfb04863a793955f848ba1624a8
url: "https://github.com/cypherstack/bip47.git"
source: git
version: "2.1.0"
@@ -349,8 +349,8 @@ packages:
dependency: "direct overridden"
description:
path: coinlib
- ref: "5c59c7e7d120d9c981f23008fa03421d39fe8631"
- resolved-ref: "5c59c7e7d120d9c981f23008fa03421d39fe8631"
+ ref: a3c972ce0b71b45afe17576d39831fe370ce7ce7
+ resolved-ref: a3c972ce0b71b45afe17576d39831fe370ce7ce7
url: "https://github.com/cypherstack/coinlib"
source: git
version: "4.1.0"
@@ -358,8 +358,8 @@ packages:
dependency: "direct main"
description:
path: coinlib_flutter
- ref: "5c59c7e7d120d9c981f23008fa03421d39fe8631"
- resolved-ref: "5c59c7e7d120d9c981f23008fa03421d39fe8631"
+ ref: a3c972ce0b71b45afe17576d39831fe370ce7ce7
+ resolved-ref: a3c972ce0b71b45afe17576d39831fe370ce7ce7
url: "https://github.com/cypherstack/coinlib"
source: git
version: "4.0.0"
@@ -536,66 +536,66 @@ packages:
dependency: "direct main"
description:
name: cs_salvium_flutter_libs
- sha256: "05a9f9e3f8cb539a310419d49270492e84d0f89bccb4c31512c854b1fe1f1c5f"
+ sha256: ac02985a3b9791979d82126f9c7a3a0f239f0cbfed5346be5a2c30b36e53c737
url: "https://pub.dev"
source: hosted
- version: "2.0.1"
+ version: "3.0.1"
cs_salvium_flutter_libs_android:
dependency: transitive
description:
name: cs_salvium_flutter_libs_android
- sha256: ad9537942f7c1416fbb3432cb154d641262bd18c56471c4f62dd1d2e7e23f125
+ sha256: "879706067b32450fe299fb558ad08d6b33cc2ea25a5ffe05ec38346b21e7d60a"
url: "https://pub.dev"
source: hosted
- version: "2.0.0"
+ version: "3.0.1"
cs_salvium_flutter_libs_android_arm64_v8a:
dependency: transitive
description:
name: cs_salvium_flutter_libs_android_arm64_v8a
- sha256: "4c307cd3276c7aa2a461ebcfc726adf9b4d9427dbdbad120dbe50f54d3690b4e"
+ sha256: "2b0d8047fd777a4a40b60f23310be20dafccbda0f5577465300f3128d90ad5d3"
url: "https://pub.dev"
source: hosted
- version: "2.0.0"
+ version: "3.0.0"
cs_salvium_flutter_libs_android_armeabi_v7a:
dependency: transitive
description:
name: cs_salvium_flutter_libs_android_armeabi_v7a
- sha256: "9491e0cdd4452c9c907e137acd2d08f76d33efc7a9d4b86fbfab69224bc9f473"
+ sha256: fb48829fdc52c4cbc71390dcb45a09fdd4e5dddb377bbd3a6b723225be6ea596
url: "https://pub.dev"
source: hosted
- version: "2.0.0"
+ version: "3.0.0"
cs_salvium_flutter_libs_android_x86_64:
dependency: transitive
description:
name: cs_salvium_flutter_libs_android_x86_64
- sha256: "0b87ccd86bd9b0eeb659dade948d076cddf908d535fe803b769030da8ff406dc"
+ sha256: "3956342b7fc1e2edf9759d2eaf084909dc0a22e5545bc6b962bbdf59c14e23cf"
url: "https://pub.dev"
source: hosted
- version: "2.0.0"
+ version: "3.0.0"
cs_salvium_flutter_libs_ios:
dependency: transitive
description:
name: cs_salvium_flutter_libs_ios
- sha256: aa474e7da65ba36e23afc4936ffbe39328808619fbdac44dacad9aa3aafb1b08
+ sha256: "5917178148b04f642e604ad8acba041a96f752689a75d7074690a46b6207d3d8"
url: "https://pub.dev"
source: hosted
- version: "2.0.1"
+ version: "3.0.0"
cs_salvium_flutter_libs_linux:
dependency: transitive
description:
name: cs_salvium_flutter_libs_linux
- sha256: "8adc16e9d0fb8dc439475ddb2eaa4fcde8433fa2cb6e14ce814b1a40965eda5c"
+ sha256: "5722e9024cb269cb59b6cc4b1df605ddddb432afff04cf3c9bd513c5fbe91be7"
url: "https://pub.dev"
source: hosted
- version: "2.0.0"
+ version: "3.0.0"
cs_salvium_flutter_libs_macos:
dependency: transitive
description:
name: cs_salvium_flutter_libs_macos
- sha256: "988077e7affc6443a1b665bac6df3b39269cc1352375cb805bd6d26aac82b46f"
+ sha256: "4413f1f6dfec97574326fc004ea4849c855163d95763a1109cfe9edfc59e2951"
url: "https://pub.dev"
source: hosted
- version: "2.0.1"
+ version: "3.0.0"
cs_salvium_flutter_libs_platform_interface:
dependency: transitive
description:
@@ -608,10 +608,10 @@ packages:
dependency: transitive
description:
name: cs_salvium_flutter_libs_windows
- sha256: "934a1eeb95619df9e23eff13a6a6a356322297abfa6ab871283cdf665cc32c7f"
+ sha256: "87f354e0103919022d2376b4305c424eb48289ffe90995553d708bbcce819a79"
url: "https://pub.dev"
source: hosted
- version: "2.0.0"
+ version: "3.0.0"
cs_wownero:
dependency: "direct main"
description:
@@ -1201,7 +1201,7 @@ packages:
path: "crypto_plugins/frostdart"
relative: true
source: path
- version: "0.0.1"
+ version: "0.2.0"
fuchsia_remote_debug_protocol:
dependency: transitive
description: flutter
@@ -1602,10 +1602,10 @@ packages:
dependency: "direct main"
description:
name: meta
- sha256: "23f08335362185a5ea2ad3a4e597f1375e78bce8a040df5c600c8d3552ef2394"
+ sha256: "1741988757a65eb6b36abe716829688cf01910bbf91c34354ff7ec1c3de2b349"
url: "https://pub.dev"
source: hosted
- version: "1.17.0"
+ version: "1.18.0"
mime:
dependency: transitive
description:
@@ -2276,26 +2276,26 @@ packages:
dependency: transitive
description:
name: test
- sha256: "280d6d890011ca966ad08df7e8a4ddfab0fb3aa49f96ed6de56e3521347a9ae7"
+ sha256: "8d9ceddbab833f180fbefed08afa76d7c03513dfdba87ffcec2718b02bbcbf20"
url: "https://pub.dev"
source: hosted
- version: "1.30.0"
+ version: "1.31.0"
test_api:
dependency: transitive
description:
name: test_api
- sha256: "8161c84903fd860b26bfdefb7963b3f0b68fee7adea0f59ef805ecca346f0c7a"
+ sha256: "949a932224383300f01be9221c39180316445ecb8e7547f70a41a35bf421fb9e"
url: "https://pub.dev"
source: hosted
- version: "0.7.10"
+ version: "0.7.11"
test_core:
dependency: transitive
description:
name: test_core
- sha256: "0381bd1585d1a924763c308100f2138205252fb90c9d4eeaf28489ee65ccde51"
+ sha256: "1991d4cfe85d5043241acac92962c3977c8d2f2add1ee73130c7b286417d1d34"
url: "https://pub.dev"
source: hosted
- version: "0.6.16"
+ version: "0.6.17"
tezart:
dependency: "direct main"
description:
Why this scored 31/100
Community notes
Notes can correct, qualify, or add evidence to the AI analysis. Every note shown here has been validated by a human moderator.
The AI analysis stands alone for now. Submit a note if you can add evidence or important context.