Skip to content

UnifiedPayment::send applies the caller's amount inconsistently for fixed-amount instructions #1201

Open

Description

@jkczyz

At main c32d3dd53638bce3d30522ffb9d52ad674f4578f, UnifiedPayment::send handles a caller-supplied amount inconsistently for PaymentInstructions::FixedAmount. The FixedAmount arm only rejects an amount below the instruction's amount. A larger amount then reaches the BOLT 12 branch, which calls send_using_amount whenever an amount is present and so pays the caller's larger value, whereas the BOLT 11 branch ignores the caller's amount and pays the invoice amount. The same URI therefore pays different amounts depending on which method succeeds, and an integration that always forwards a user-entered or default amount can overpay a fixed-amount offer without noticing.

Decide on one behavior for fixed-amount instructions and apply it across methods: either use send_using_amount only for ConfigurableAmount and call Bolt12Payment::send for fixed offers, or reject any caller amount that differs from the fixed amount up front with Error::InvalidAmount. Extend unified_send_receive_bip21_uri to send a fixed-amount URI with a larger amount and assert either an InvalidAmount error or that the receiver gets exactly the fixed amount, for both the BOLT 12 and the BOLT 11 fallback paths. The behavior was verified by reading; no PoC was run during triage.

This finding was discovered by Project Loupe.

Activity

  1. Ansh-699 commented on Oct 27, 2026

    @Ansh-699
    Contributor

    I'd like to take this one.

    Reproduced on 9386a1c: a 100k sat BIP 21 URI paid with amount_msat = 200_000_000 sends 200k sats over BOLT 12 but 100k over the BOLT 11 fallback.

    I'd pay the resolved amount on every path, so BOLT 12 uses resolved.ln_payment_amount() like on-chain already uses onchain_payment_amount(). A larger caller amount is then ignored everywhere, and a smaller one is still rejected up front by the check from #607. Rejecting any differing amount would work too, but it'd start failing apps that pass a default amount today. Happy to go that way if you'd rather.

    Two other things I noticed:

    • An HRN resolving to a fixed-amount offer with no caller amount passes 0 to send_using_amount_inner, which fails. Using the resolved amount fixes this too.
    • Amountless BOLT 11 invoices can't be paid through UnifiedPayment::send at all. set_amount only sets ln_amt, so send rejects the invoice as zero-amount and we return PaymentSendingFailed (reproduced). Same PR, or separate?
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions