Skip to content

[Bug]: on-chain send can drain full balance when dust change triggers send-all #536

@piotr-iohk

Description

@piotr-iohk

What happened?

While testing public Paykit/contact payments, I tried to send 5,000 sats on-chain to a contact. The transaction sent the whole on-chain balance instead.

This does not look Paykit-specific. Logs show the send flow still requested 5,000 sats, but the iOS on-chain send path passed isMaxAmount: true because selected UTXOs would have left dust change. That caused LDK sendAllToAddress to run and drain the full wallet balance.

Observed transaction:

5a1b6a9df66368acc1a14a93f24b34d266a763c5adfdfb44f89b9041041c36a5

Relevant log findings:

  • Balance before send: onchain=72394, lightning=23354.
  • UTXO selection target: 5000 sats.
  • Selected UTXOs: 2810 + 2640 = 5450 sats.
  • Fee estimate for selected UTXOs: 181 sats.
  • Expected change would be 269 sats, below dust.
  • Final send call: Sending 5000 sats ... (isMaxAmount: true).
  • LDK created tx by sending available on-chain funds retaining a reserve of 0sats.
  • Wallet event detected amount: -72394 sats.
  • Balance changed: onchain 72394 -> 0.

Expected behavior

Sending 5,000 sats should not drain the whole wallet balance.

If selected UTXOs would leave dust change, the app should either:

  • select different UTXOs,
  • absorb only the selected-input dust into fee/recipient amount with clear UI,
  • show an error and ask the user to adjust amount/coin selection,
  • or otherwise avoid calling a full-wallet send-all path unless the user explicitly selected max amount.

Steps to Reproduce

  1. Use an iOS wallet with multiple on-chain UTXOs.
  2. Start an on-chain send to a contact or normal address.
  3. Enter an amount that causes selected UTXOs to leave dust change. In the observed case:
    • amount: 5,000 sats
    • selected UTXOs total: 5,450 sats
    • estimated fee: 181 sats
    • dust change: 269 sats
  4. Confirm/send the transaction.
  5. Observe that the transaction drains the full on-chain balance instead of sending only the requested amount.

Logs / Screenshots / Recordings

Attachment prepared:

bitkit_logs_2026-05-05_12-15-22.zip

Key txid:

  • 5a1b6a9df66368acc1a14a93f24b34d266a763c5adfdfb44f89b9041041c36a5

Bitkit Version

2.2.0

Observed on bitkit-ios branch codex-paykit-public-endpoints-pr527 at revision c58922938b3d.

Device / OS

iPhone 17 Simulator.

Reproducibility

Rarely (once or twice)

Observed once during Paykit contact-payment testing, but the log path suggests it should be reproducible whenever selected on-chain UTXOs leave dust change and shouldUseSendAll becomes true for a non-max send.

Additional context

Likely source:

  • SendConfirmationView.swift computes useMaxAmount = wallet.isMaxAmountSend || shouldUseSendAll.
  • shouldUseSendAll can become true when selected UTXOs would leave dust change.
  • WalletViewModel.send(... isMaxAmount: true) passes this to LightningService.send.
  • LightningService.send(... isMaxAmount: true) calls sendAllToAddress, which sends all available on-chain funds rather than only the selected UTXOs.

This was discovered while paying an on-chain public Paykit/contact endpoint, but the failure mode appears to be generic iOS on-chain dust/send-all handling rather than Paykit-specific.

Similar issues check

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions