Skip to content

Reject self-transfer (A->A) swap legs#444

Merged
LandynDev merged 2 commits into
testfrom
feature/reject-self-transfer-swaps
Jun 1, 2026
Merged

Reject self-transfer (A->A) swap legs#444
LandynDev merged 2 commits into
testfrom
feature/reject-self-transfer-swaps

Conversation

@anderdc
Copy link
Copy Markdown
Collaborator

@anderdc anderdc commented Jun 1, 2026

What

A swap leg's payer and payee are always distinct parties, so a transaction whose sender equals the recipient delivers nothing to anyone. Without a guard, an operator who lines up the miner's committed address with the user's address could "fulfill" a swap with a same-wallet self-send and manufacture fake volume.

Surfaced while diagnosing swap 2085 (btc→tao): it timed out and was slashed despite a posted SwapFulfilled. The destTxHash decoded to a Balances.transfer_keep_alive from the user to the user's own address (5H3Symui… → 5H3Symui…) for the net amount — not a miner→user delivery. It only failed because the operator used the wrong wallet on the TAO leg; a smarter setup (committing miner_to_address == user_to_address) would have completed.

Changes

  • chain_providers/base.pyverify_transaction rejects sender == expected_recipient. This is the on-chain choke point both source-confirm and dest-confirm route through, so it catches A→A regardless of how addresses were pinned.
  • validator/axon_handlers.py — fail earlier so a self-flow swap is never created:
    • handle_swap_reserve: reject when the user from_address matches the miner's committed address (source leg, before a reservation is held).
    • handle_swap_confirm: reject when from_address == miner_from_address or to_address == miner_fulfillment_address (both legs, before vote_initiate).
  • tests/test_self_transfer_guard.py — A→A rejected (with and without a pinned expected_sender); A→B passes.

Scope / non-goals

  • A→B self-flow between two operator-owned wallets is indistinguishable on-chain and still passes by design — it pays real fees and is bounded on the reward side (breadth/depth shaping), not here.
  • No fund-theft implication; this is anti-fake-volume / anti-self-deal hardening.

Test

pytest tests/test_self_transfer_guard.py tests/test_axon_handlers.py tests/test_forward.py tests/test_chain_verification.py → all green.

anderdc and others added 2 commits June 1, 2026 15:21
A swap leg's payer and payee are always distinct parties, so a tx whose
sender equals the recipient delivers nothing. Without a guard, an operator
who lines up the miner's committed address with the user's address could
fulfill a swap with a same-wallet self-send and manufacture fake volume.

- verify_transaction: reject when sender == expected_recipient (covers both
  source-confirm and dest-confirm legs at the on-chain choke point).
- handle_swap_reserve / handle_swap_confirm: reject when a user address equals
  the miner's committed deposit/payout address, so a self-flow operator can't
  even hold a reservation or create the swap.

A->B self-flow between two operator-owned wallets is indistinguishable
on-chain and still passes; it pays real fees and is bounded on the reward side.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants