Merge pull request #1452 from levoncrypto/masternode-operator-reward
What changed, and why it matters
This commit removes the user-facing 'operator reward' field from the Firo masternode registration screen and hard-codes that value to zero in the wallet logic. It is a feature removal rather than a fix for an active security flaw, but it does close off a potential input-validation and transaction-construction risk surface.
No urgent action required. Treat as a routine hardening/feature-simplification change. If the operator reward feature is reintroduced later, ensure strict input validation, fuzz testing of the basis-point conversion, and clear UX warnings about the 0-10000 range.
Security signals we found
Removal of user-supplied numeric field that directly influenced on-chain transaction payload (nOperatorReward basis points)
Elimination of locale-dependent decimal parsing and rounding path for a consensus-relevant value
Hard-coding of a transaction field that previously had range/validation checks
Evidence from the diff
The patch deletes the operator reward percentage input from RegisterMasternodeForm and removes the operatorReward parameter from FiroWallet.registerMasternode(…), hard-coding nOperatorReward to 0 in the serialized ProRegTx payload. Previously the UI parsed a locale-aware decimal percentage, converted it to basis points (0-10000), and passed it to the wallet. That path is now gone, so any bug or manipulation involving operator reward values can no longer be triggered through this UI flow.
Changed components
lib/pages/masternodes/sub_widgets/register_masternode_form.dartlib/wallets/wallet/impl/firo_wallet.dartFiro masternode ProRegTx constructionInspect captured patch +1 / −62
### lib/pages/masternodes/sub_widgets/register_masternode_form.dart
@@ -1,12 +1,7 @@
-import 'package:decimal/decimal.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
-import '../../../providers/global/locale_provider.dart';
import '../../../providers/global/wallets_provider.dart';
-import '../../../utilities/amount/amount.dart';
-import '../../../utilities/amount/amount_field_relocalization.dart';
-import '../../../utilities/amount/amount_input_formatter.dart';
import '../../../themes/stack_colors.dart';
import '../../../utilities/if_not_already.dart';
import '../../../utilities/logger.dart';
@@ -48,7 +43,6 @@ class _RegisterMasternodeFormState
final _ipAndPortController = TextEditingController();
final _operatorPubKeyController = TextEditingController();
final _votingAddressController = TextEditingController();
- final _operatorRewardController = TextEditingController(text: "0");
final _payoutAddressController = TextEditingController();
TextStyle _getStyle(BuildContext context) {
@@ -65,17 +59,8 @@ class _RegisterMasternodeFormState
bool _enableCreateButton = false;
- // Parse as a 2-decimal Amount so pasted overprecision ("0.001") is
- // rejected instead of silently rounding to zero basis points.
- Decimal? get _operatorRewardPercent => Amount.tryParseEditableAmount(
- _operatorRewardController.text,
- locale: ref.read(localeServiceChangeNotifierProvider).locale,
- fractionDigits: 2,
- )?.decimal;
-
void _validate() {
if (mounted) {
- final percent = _operatorRewardPercent;
setState(() {
_enableCreateButton = [
_ipAndPortController.text
@@ -85,7 +70,6 @@ class _RegisterMasternodeFormState
.length ==
2,
_operatorPubKeyController.text.trim().isNotEmpty,
- percent != null && percent <= Decimal.fromInt(100),
_payoutAddressController.text.trim().isNotEmpty,
].every((e) => e);
});
@@ -100,18 +84,6 @@ class _RegisterMasternodeFormState
final votingAddress = _votingAddressController.text.trim();
final payoutAddress = _payoutAddressController.text.trim();
- // according to https://github.com/cypherstack/stack_wallet/blob/c898a70f808ed5490b8dd23571f5f162d9e38158/lib/wallets/wallet/impl/firo_wallet.dart#L1064
- // this should be a percent of 10000
- final operatorPercent = _operatorRewardPercent;
- if (operatorPercent == null) {
- throw Exception("Invalid operator reward");
- }
- final operatorReward = (operatorPercent * Decimal.fromInt(100))
- .round()
- .toBigInt()
- .toInt()
- .clamp(0, 10000);
-
final wallet =
ref.read(pWallets).getWallet(widget.firoWalletId) as FiroWallet;
@@ -120,7 +92,6 @@ class _RegisterMasternodeFormState
port,
operatorPubKey,
votingAddress,
- operatorReward,
payoutAddress,
collateralTxid: widget.collateralTxid,
collateralVout: widget.collateralVout,
@@ -177,7 +148,6 @@ class _RegisterMasternodeFormState
_ipAndPortController.dispose();
_operatorPubKeyController.dispose();
_votingAddressController.dispose();
- _operatorRewardController.dispose();
_payoutAddressController.dispose();
super.dispose();
}
@@ -186,12 +156,6 @@ class _RegisterMasternodeFormState
Widget build(BuildContext context) {
final stack = Theme.of(context).extension<StackColors>()!;
- listenForAmountRelocalization(
- ref.listen,
- controllers: [_operatorRewardController],
- onRelocalized: _validate,
- );
-
return Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
@@ -267,28 +231,6 @@ class _RegisterMasternodeFormState
),
SizedBox(height: Util.isDesktop ? 24 : 16),
- SelectableText("Operator reward (%)", style: _getStyle(context)),
- SizedBox(height: Util.isDesktop ? 10 : 8),
- AdaptiveTextField(
- controller: _operatorRewardController,
- showPasteClearButton: true,
- maxLines: 1,
- keyboardType: const TextInputType.numberWithOptions(decimal: true),
- inputFormatters: [
- AmountInputFormatter(
- controller: _operatorRewardController,
- decimals: 2,
- locale: ref.watch(
- localeServiceChangeNotifierProvider.select(
- (value) => value.locale,
- ),
- ),
- ),
- ],
- onChangedComprehensive: (_) => _validate(),
- ),
- SizedBox(height: Util.isDesktop ? 24 : 16),
-
SelectableText("Payout address", style: _getStyle(context)),
SizedBox(height: Util.isDesktop ? 10 : 8),
AdaptiveTextField(
### lib/wallets/wallet/impl/firo_wallet.dart
@@ -970,7 +970,6 @@ class FiroWallet<T extends ElectrumXCurrencyInterface> extends Bip39HDWallet<T>
int port,
String operatorPubKey,
String votingAddress,
- int operatorReward,
String payoutAddress, {
required String collateralTxid,
required int collateralVout,
@@ -1131,9 +1130,7 @@ class FiroWallet<T extends ElectrumXCurrencyInterface> extends Bip39HDWallet<T>
}
// nOperatorReward (16 bit)
- if (operatorReward < 0 || operatorReward > 10000) {
- throw Exception("Invalid operator reward: $operatorReward");
- }
+ const operatorReward = 0;
registrationTx.add(
(ByteData(
2,Why this scored 30/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.