Skip to content

Swaps: a rescue host can set the claim address of rescued reverse swaps #4943

Closed

Description

@kaloudis

Problem

When rescuing swaps, SwapStore.getRescuableSwaps (stores/SwapStore.ts:1046) copies every field of the swap host's /swap/restore response into the stored swap. Only claimDetails and refundDetails are skipped. It then sets keys, endpoint, implementation, nodePubkey and, for reverse swaps, preimage, but it never overrides destinationAddress.

The claim path uses the stored value as is:

  • SwapDetails passes swapData.destinationAddress to createReverseClaimTransaction (views/Swaps/SwapDetails.tsx:531).
  • resolveClaimAddress (stores/SwapStore.ts:838) returns a stored destinationAddress without checking it, and only generates a wallet address when the field is empty.

There is no check that the address belongs to the wallet or is on the node's network.

Scenario

  1. A user has a reverse swap whose hold invoice they have paid, loses app data, and runs the rescue with their rescue key against the configured swap host.
  2. The host adds "destinationAddress": "bc1q<host address>" to that swap in the /swap/restore response.
  3. The user claims. Zeus builds the claim with the re-derived preimage and sends the lockup output to the host's address.
  4. The host now has the preimage. It settles the user's hold invoice and gets its lockup back through the claim.

The user loses the invoice amount and receives nothing on chain.

Before 66aedd3 (#4460), rescued reverse claims were built with an empty preimage and always failed, so this path could not be reached. It can be from v13.2.3 on. #4894 (lockup verification) does not cover it: it checks the lockup address and tree, not the claim destination.

Related: keyIndex is taken from the host

The rescue derives the key and preimage from claimDetails.keyIndex as sent by the host (stores/SwapStore.ts:1050). Nothing checks that sha256(preimage) equals the swap's preimageHash or that the derived public key matches the claim key in the swap tree. A host could return another live swap's keyIndex. With #4894 applied, the lockup check binds the lockup script to the derived preimage hash and key, which limits this. The rescue should still reject the mismatch directly.

Proposed fix

In getRescuableSwaps:

  1. Build the stored swap from an allow-list of host fields (id, type, status, lockup address, swap tree, timeout, amounts, preimage hash, server public key) instead of copying everything. At minimum, drop destinationAddress, preimage, keys, refundPrivateKey, refundPublicKey and anything else the client sets itself.
  2. For reverse swaps, skip the swap unless sha256(derived preimage) === preimageHash and the derived public key matches the claim public key in claimDetails.
  3. Leave destinationAddress unset so resolveClaimAddress generates a wallet address at claim time, as it already does for swaps rescued without one.

Tests

In stores/SwapStore.rescue.test.ts:

  • A restore response with destinationAddress set saves the swap without it, and resolveClaimAddress then returns a fresh wallet address.
  • Host-sent preimage, keys and refundPrivateKey are not stored.
  • A reverse swap whose preimageHash does not match the re-derived preimage is skipped.
  • A reverse swap whose claim public key does not match the key at keyIndex is skipped.
  • A valid reverse swap and a submarine swap are still rescued with the same stored fields as today, apart from the dropped ones.

Found during the v13.2.3 pre-release review.

Activity

  1. added this to the v13.2.3 milestone on Oct 8, 2026
  2. self-assigned this
    on Oct 8, 2026
  3. added 2 commits that reference this issue on Oct 8, 2026
    c83e1d7
    ec1fb7d
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

Type

No type

Projects

No projects

    Milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions