What changed, and why it matters
This commit fixes a bug in the Stack Wallet app where Firo (a cryptocurrency) address validation was incorrectly accepting Bitcoin-style 'bc1' and 'tb1' addresses. Because Firo does not use SegWit/Bech32 addresses, treating these as valid could let a user accidentally send Firo funds to a Bitcoin address, likely resulting in permanent loss of money. The patch changes the wallet to reject Bitcoin Bech32 addresses and adds tests to make sure it does so.
Review whether any other coins in Stack Wallet use an incorrect bech32Hrp or could accept cross-chain Bech32 addresses, and audit related UI flows (send/receive/address book) to ensure rejected addresses cannot be used in transactions. Consider adding integration tests that attempt to construct a transaction to a rejected address.
Security signals we found
Address-validation bypass for cross-chain addresses
Potential loss of funds from sending to wrong-chain Bech32 address
Incorrect Bech32 human-readable part configured for Firo network
Defensive input validation hardening
Evidence from the diff
The Firo coin implementation previously set bech32Hrp to ‘bc’ (mainnet) and ‘tb’ (testnet), which are Bitcoin’s human-readable parts, and its fallback validation path could accept Bitcoin Bech32 addresses. The patch empties the bech32Hrp, adds an explicit helper _isBitcoinBech32Address() that flags ‘bc1’/’tb1’ strings, and returns false/null for those addresses in validateAddress() and getAddressType(). It also reorders the fallback checks so exchange and Spark addresses are still accepted. New unit tests verify that Firo transparent and exchange addresses are accepted while Bitcoin Bech32 addresses are rejected.
Changed components
lib/wallets/crypto_currency/coins/firo.dartFiro address validation logicFiro address type detectiontest/wallets/firo_address_validation_test.dartInspect captured patch +70 / −11
diff --git a/lib/wallets/crypto_currency/coins/firo.dart b/lib/wallets/crypto_currency/coins/firo.dart
index 583dc4b..fb470c6 100644
--- a/lib/wallets/crypto_currency/coins/firo.dart
+++ b/lib/wallets/crypto_currency/coins/firo.dart
@@ -13,6 +13,11 @@ import '../crypto_currency.dart';
import '../interfaces/electrumx_currency_interface.dart';
import '../intermediate/bip39_hd_currency.dart';
+bool _isBitcoinBech32Address(String address) {
+ final value = address.toLowerCase();
+ return value.startsWith("bc1") || value.startsWith("tb1");
+}
+
class Firo extends Bip39HDCurrency with ElectrumXCurrencyInterface {
Firo(super.network) {
_idMain = "firo";
@@ -106,7 +111,7 @@ class Firo extends Bip39HDCurrency with ElectrumXCurrencyInterface {
p2shPrefix: 0x07,
privHDPrefix: 0x0488ade4,
pubHDPrefix: 0x0488b21e,
- bech32Hrp: "bc",
+ bech32Hrp: "",
messagePrefix: '\x16Zcoin Signed Message:\n',
minFee: BigInt.from(1), // Not used in stack wallet currently
minOutput: dustLimit.raw, // Not used in stack wallet currently
@@ -119,7 +124,7 @@ class Firo extends Bip39HDCurrency with ElectrumXCurrencyInterface {
p2shPrefix: 0xb2,
privHDPrefix: 0x04358394,
pubHDPrefix: 0x043587cf,
- bech32Hrp: "tb",
+ bech32Hrp: "",
messagePrefix: "\x16Zcoin Signed Message:\n",
minFee: BigInt.from(1), // Not used in stack wallet currently
minOutput: dustLimit.raw, // Not used in stack wallet currently
@@ -188,11 +193,8 @@ class Firo extends Bip39HDCurrency with ElectrumXCurrencyInterface {
coinlib.Address.fromString(address, networkParams);
return true;
} catch (_) {
- if (validateSparkAddress(address)) {
- return true;
- } else {
- return isExchangeAddress(address);
- }
+ if (_isBitcoinBech32Address(address)) return false;
+ return isExchangeAddress(address) || validateSparkAddress(address);
}
}
@@ -301,9 +303,9 @@ class Firo extends Bip39HDCurrency with ElectrumXCurrencyInterface {
@override
AddressType? getAddressType(String address) {
- if (validateSparkAddress(address)) {
- return .spark;
- }
- return super.getAddressType(address);
+ if (_isBitcoinBech32Address(address)) return null;
+ final type = super.getAddressType(address);
+ if (type != null) return type;
+ return validateSparkAddress(address) ? .spark : null;
}
}
diff --git a/test/wallets/firo_address_validation_test.dart b/test/wallets/firo_address_validation_test.dart
new file mode 100644
index 0000000..74e668c
--- /dev/null
+++ b/test/wallets/firo_address_validation_test.dart
@@ -0,0 +1,57 @@
+import "package:coinlib_flutter/coinlib_flutter.dart" as coinlib;
+import "package:flutter_test/flutter_test.dart";
+import "package:stackwallet/models/isar/models/blockchain_data/address.dart";
+import "package:stackwallet/wallets/crypto_currency/crypto_currency.dart";
+
+void main() {
+ final mainnet = Firo(CryptoCurrencyNetwork.main);
+ final testnet = Firo(CryptoCurrencyNetwork.test);
+
+ test("accepts Firo transparent addresses", () {
+ expect(
+ mainnet.validateAddress("a8VV7vMzJdTQj1eLEJNskhLEBUxfNWhpAg"),
+ isTrue,
+ );
+ expect(
+ mainnet.getAddressType("a8VV7vMzJdTQj1eLEJNskhLEBUxfNWhpAg"),
+ AddressType.p2pkh,
+ );
+ expect(
+ testnet.validateAddress("THqfkegzJjpF4PQFAWPhJWMWagwHecfqva"),
+ isTrue,
+ );
+ expect(
+ testnet.getAddressType("THqfkegzJjpF4PQFAWPhJWMWagwHecfqva"),
+ AddressType.p2pkh,
+ );
+ });
+
+ test("rejects Bitcoin Bech32 addresses", () {
+ const mainnetBitcoin = "bc1qc5ymmsay89r6gr4fy2kklvrkuvzyln4shdvjhf";
+ const testnetBitcoin = "tb1qzzlm6mnc8k54mx6akehl8p9ray8r439va5ndyq";
+
+ expect(mainnet.validateAddress(mainnetBitcoin), isFalse);
+ expect(mainnet.getAddressType(mainnetBitcoin), isNull);
+ expect(
+ () => coinlib.Address.fromString(mainnetBitcoin, mainnet.networkParams),
+ throwsA(anything),
+ );
+ expect(testnet.validateAddress(testnetBitcoin), isFalse);
+ expect(testnet.getAddressType(testnetBitcoin), isNull);
+ expect(
+ () => coinlib.Address.fromString(testnetBitcoin, testnet.networkParams),
+ throwsA(anything),
+ );
+ });
+
+ test("keeps Firo exchange addresses", () {
+ expect(
+ mainnet.validateAddress("EXXMGtieRLNGfgewJ4jJCN4kZFTUcjYMDdHs"),
+ isTrue,
+ );
+ expect(
+ testnet.validateAddress("EXTKtrsZSTGU2vUbuCV6sBDVqPAS3JQkaYJ3"),
+ isTrue,
+ );
+ });
+}
Why this scored 59/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.