Repository navigation
Swaps: a rescue host can set the claim address of rescued reverse swaps #4943
Description
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:
SwapDetailspassesswapData.destinationAddresstocreateReverseClaimTransaction(views/Swaps/SwapDetails.tsx:531).resolveClaimAddress(stores/SwapStore.ts:838) returns a storeddestinationAddresswithout 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
- 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.
- The host adds
"destinationAddress": "bc1q<host address>"to that swap in the/swap/restoreresponse. - The user claims. Zeus builds the claim with the re-derived preimage and sends the lockup output to the host's address.
- 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:
- 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,refundPublicKeyand anything else the client sets itself. - For reverse swaps, skip the swap unless
sha256(derived preimage) === preimageHashand the derived public key matches the claim public key inclaimDetails. - Leave
destinationAddressunset soresolveClaimAddressgenerates 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
destinationAddressset saves the swap without it, andresolveClaimAddressthen returns a fresh wallet address. - Host-sent
preimage,keysandrefundPrivateKeyare not stored. - A reverse swap whose
preimageHashdoes not match the re-derived preimage is skipped. - A reverse swap whose claim public key does not match the key at
keyIndexis 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
swaps: take only the host's side of a rescued swap from the host
swaps: take only the host's side of a rescued swap from the host