What changed, and why it matters
This commit fixes a bug in the Skylight Wallet's Monero receive screen where the wallet could display one subaddress while labeling it with the index of a different subaddress. The fix bundles the address and its index into a single value so they are always read together consistently. If left unfixed, a user could copy and share a receiving address that is mismatched with the label, potentially causing payments to be credited to the wrong wallet entry or making reconciliation confusing. There is no direct evidence of malicious exploitation; it appears to be a correctness and usability fix.
Treat as a low-to-moderate correctness fix. Review whether any cached or displayed address/index pairs could have been mismatched in prior builds, and consider adding a regression test that asserts the displayed label index matches the address index. No emergency response is indicated.
Security signals we found
UI label/address desynchronization bug fixed
Atomic read of paired values (address + index)
Demo mode now explicitly bypasses subaddress lookup
No explicit security framing by vendor in commit message
Evidence from the diff
The receive screen previously called wallet.getUnusedSubaddress() and wallet.unusedSubaddressIndex separately. Because these are independent async-backed getters, a state update between reads could yield an address from one subaddress index paired with the label of another. The patch introduces a new getter unusedSubaddress that returns a record ({int index, String address}) from the underlying Monero wallet adapter, ensuring the address and its index are read atomically. The receive screen now uses this combined value and skips subaddress lookup in demo mode.
Changed components
lib/screens/receive.dartlib/models/app_wallet.dartlib/models/monero_wallet_adapter.dartInspect captured patch +10 / −5
### lib/models/app_wallet.dart
@@ -108,6 +108,9 @@ abstract interface class AppWallet implements Listenable {
// Receive (serverSupportsSubaddresses is declared with the sync getters above)
String? getUnusedSubaddress();
+
+ /// The subaddress to hand out with the index it actually is, as one value.
+ ({int index, String address})? get unusedSubaddress;
int? get unusedSubaddressIndex;
bool? get unusedSubaddressIndexIsSupported;
### lib/models/monero_wallet_adapter.dart
@@ -84,6 +84,8 @@ class MoneroWalletAdapter extends ChangeNotifier implements AppWallet {
@override
String? getUnusedSubaddress() => _wallet.getUnusedSubaddress();
@override
+ ({int index, String address})? get unusedSubaddress => _wallet.unusedSubaddress;
+ @override
int? get unusedSubaddressIndex => _wallet.unusedSubaddressIndex;
@override
bool? get unusedSubaddressIndexIsSupported => _wallet.unusedSubaddressIndexIsSupported;
### lib/screens/receive.dart
@@ -73,8 +73,10 @@ class _ReceiveScreenState extends State<ReceiveScreen> {
final i18n = AppLocalizations.of(context)!;
final wallet = appWalletOf(context, listen: true);
final primaryAddress = wallet.getPrimaryAddress();
- final subaddress = wallet.getUnusedSubaddress();
final isDemoMode = wallet.connectionAddress == 'demo';
+ // The address and the index that labels it, as one value: read separately
+ // they can name different subaddresses.
+ final sub = isDemoMode ? null : wallet.unusedSubaddress;
final subSupported = wallet.serverSupportsSubaddresses;
final canToggle = subSupported == true && !isDemoMode;
@@ -83,7 +85,7 @@ class _ReceiveScreenState extends State<ReceiveScreen> {
address = primaryAddress;
}
if (subSupported == true) {
- address = _showSubaddress ? subaddress : primaryAddress;
+ address = _showSubaddress ? sub?.address : primaryAddress;
}
final ready = (subSupported != null || isDemoMode) && address != null;
@@ -104,9 +106,7 @@ class _ReceiveScreenState extends State<ReceiveScreen> {
onSelectTab: (index) => setState(() => _showSubaddress = index == 0),
address: address ?? '',
qrHeading: canToggle && _showSubaddress
- ? (wallet.unusedSubaddressIndex != null
- ? '${i18n.receiveSubaddressTab} #${wallet.unusedSubaddressIndex}'
- : i18n.receiveSubaddressTab)
+ ? (sub != null ? '${i18n.receiveSubaddressTab} #${sub.index}' : i18n.receiveSubaddressTab)
: i18n.receiveAddressHeading('Monero'),
warning: warning,
onCopy: () => _copyAddressToClipboard(address!),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.