fix(firo): tighten Spark mint fee accounting
What changed, and why it matters
This commit tightens how Stack Wallet calculates and checks fees when creating privacy-preserving 'Spark mint' transactions for the Firo cryptocurrency. It fixes a loop that could keep charging fees after outputs were drained, adds an error when a user tries to auto-mint all coins but the balance is too small to pay the fee, and adds a final sanity check comparing the expected fee against the actual fee built into the transaction. The changes appear aimed at preventing incorrect or excessive fees, or transactions that fail silently.
Review the full Spark mint flow to confirm the fee-subtraction loop now terminates correctly and does not over-allocate fees across outputs. Verify that the new actualFee check does not reject legitimate transactions due to rounding or off-by-one errors. Consider adding unit tests for edge cases such as tiny UTXOs, multiple outputs, and autoMintAll mode.
Security signals we found
Fee accounting mismatch check added between estimated and actual transaction fee
Loop boundary fix in fee subtraction from outputs
Explicit failure added for auto-mint-all when UTXO value is insufficient to cover fee
Potential prevention of over-payment or under-payment of miner fees in Spark mints
Evidence from the diff
The patch modifies lib/wallets/wallet/wallet_mixin_interfaces/spark_interface.dart in three ways: (1) the fee-subtraction loop now resets outputIndex to 0 when it reaches the end of singleTxOutputs and continues while outputs remain and fee remains, preventing index-out-of-range or uneven fee distribution; (2) when autoMintAll is enabled and singleTxOutputs becomes empty because the UTXO cannot cover the fee, it now throws an explicit exception instead of silently removing the coin; (3) after building the transaction, it computes actualFee as the sum of input values minus the sum of output values and throws if it does not equal the previously estimated nFeeRet, logging a mismatch message. These are defensive accounting fixes for Spark mint transaction construction.
Changed components
lib/wallets/wallet/wallet_mixin_interfaces/spark_interface.dartFiro Spark mint transaction constructionFee estimation and output allocation logicInspect captured patch +22 / −2
diff --git a/lib/wallets/wallet/wallet_mixin_interfaces/spark_interface.dart b/lib/wallets/wallet/wallet_mixin_interfaces/spark_interface.dart
index 96a63e0..ed6719d 100644
--- a/lib/wallets/wallet/wallet_mixin_interfaces/spark_interface.dart
+++ b/lib/wallets/wallet/wallet_mixin_interfaces/spark_interface.dart
@@ -1695,8 +1695,11 @@ mixin SparkInterface<T extends ElectrumXCurrencyInterface>
if (subtractFeeFromAmount && nFeeRet > BigInt.zero) {
var remainingFee = nFeeRet;
var outputIndex = 0;
- while (outputIndex < singleTxOutputs.length &&
- remainingFee > BigInt.zero) {
+ while (singleTxOutputs.isNotEmpty && remainingFee > BigInt.zero) {
+ if (outputIndex >= singleTxOutputs.length) {
+ outputIndex = 0;
+ }
+
final outputsLeft = BigInt.from(
singleTxOutputs.length - outputIndex,
);
@@ -1717,6 +1720,9 @@ mixin SparkInterface<T extends ElectrumXCurrencyInterface>
}
if (singleTxOutputs.isEmpty) {
+ if (autoMintAll) {
+ throw Exception("UTXO value is too small to cover Spark mint fee");
+ }
valueAndUTXOs.remove(itr);
skipCoin = true;
break;
@@ -2086,6 +2092,20 @@ mixin SparkInterface<T extends ElectrumXCurrencyInterface>
rethrow;
}
final builtTx = txb.build();
+ final actualFee =
+ vin
+ .map((e) => BigInt.from(e.utxo.value))
+ .fold(BigInt.zero, (p, e) => p + e) -
+ vout
+ .map((e) => BigInt.from(e.$2))
+ .fold(BigInt.zero, (p, e) => p + e);
+ if (actualFee != nFeeRet) {
+ Logging.instance.e(
+ "Spark mint fee accounting mismatch: "
+ "expected=$nFeeRet, actual=$actualFee",
+ );
+ throw Exception("Spark mint fee accounting mismatch");
+ }
// TODO: see todo at top of this function
assert(outputs.length == 1);
Why this scored 42/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.