Summary
The shipped bridge-out CLI (node/src/bin/send_bridge_out.rs) submits an LP payInvoice escrow without checking that claimData binds the user's --btc-address / --amount.
The only on-chain bind from “pegBTC locked” to “BTC the user receives” is escrow.claimData (outputAmount + keccak256(outputScript) + nonce + confirmations). Once the LP claims, EscrowStorage._finalize sets CLAIMED and refund is impossible.
A malicious / compromised LP can return escrow.amount = 0.01 pegBTC (looks right) while claimData commits outputAmount = 1 sat. The CLI copies claimData verbatim and broadcasts initialize. User locks 0.01; LP pays 1 sat and claims the full lock.
Not proven here: live payInvoice lie, live initialize, pegBTC moved. This is a complete stock path in the shipped binary, not a live steal. It is not BitVM3 Take1/Take2.
Affected component
GOATNetwork/bitvm-node gc-v2 — Bridge State Machine helper CLI ([[bin]] name = "bridge-out").
node/src/bin/send_bridge_out.rs call_pay_invoice — POSTs --btc-address / --amount to default https://152-32-185-32.nodes.atomiq.exchange:8443/tobtc/payInvoice?chain=GOAT
parse_escrow_from_pay_invoice — claim_data: parse_bytes32(&escrow_raw.claim_data, "claimData")? (verbatim)
run_swap_initialize_from_pay_invoice → submit_swap_initialize_tx — no recompute
node/src/utils.rs hash_claim_commitment already implements the exact on-chain commitment and is not called on this path
Checked on gc-v2. Not #435 (Swap watcher from_script DoS). Not #428. Not #429 / #430 / #431. Not #21.
Mechanism
// send_bridge_out.rs parse_escrow_from_pay_invoice
claim_data: parse_bytes32(&escrow_raw.claim_data, "claimData")?,
CLI sends the user's requested address/amount to the LP, then trusts the returned claimData bytes. It never:
- Decodes
outputAmount / outputScript from the commitment
- Recomputes
hash_claim_commitment from --btc-address → scriptPubKey + expected sats + nonce/confirmations
- Asserts equality with
escrow.claimData
- Prints the decoded payout before signing
On-chain claim only checks keccak of whatever script was committed. Timelock refund does not cover under-payment after a successful claim.
Impact
Per-user pegBTC theft against anyone who uses this CLI against a lying LP. Requires a malicious counterparty. Not an unauthenticated protocol path. Not BitVM3 kickoff/Take1/Take2.
Availability of a check that already exists in-repo (hash_claim_commitment) is the defect.
Suggested fix
In parse_escrow_from_pay_invoice (or immediately after), recompute hash_claim_commitment from the user's --btc-address / --amount / nonce / confirmations / btcRelay and require == escrow.claimData. Also assert escrow.amount, escrow.token, escrow.claimHandler, and refund timelock match the request before swap_initialize. Print decoded outputAmount + outputScript and refuse to submit on mismatch.
Summary
The shipped
bridge-outCLI (node/src/bin/send_bridge_out.rs) submits an LPpayInvoiceescrow without checking thatclaimDatabinds the user's--btc-address/--amount.The only on-chain bind from “pegBTC locked” to “BTC the user receives” is
escrow.claimData(outputAmount+keccak256(outputScript)+ nonce + confirmations). Once the LP claims,EscrowStorage._finalizesetsCLAIMEDand refund is impossible.A malicious / compromised LP can return
escrow.amount = 0.01 pegBTC(looks right) whileclaimDatacommitsoutputAmount = 1 sat. The CLI copiesclaimDataverbatim and broadcastsinitialize. User locks 0.01; LP pays 1 sat and claims the full lock.Not proven here: live
payInvoicelie, liveinitialize, pegBTC moved. This is a complete stock path in the shipped binary, not a live steal. It is not BitVM3 Take1/Take2.Affected component
GOATNetwork/bitvm-nodegc-v2— Bridge State Machine helper CLI ([[bin]] name = "bridge-out").node/src/bin/send_bridge_out.rscall_pay_invoice— POSTs--btc-address/--amountto defaulthttps://152-32-185-32.nodes.atomiq.exchange:8443/tobtc/payInvoice?chain=GOATparse_escrow_from_pay_invoice—claim_data: parse_bytes32(&escrow_raw.claim_data, "claimData")?(verbatim)run_swap_initialize_from_pay_invoice→submit_swap_initialize_tx— no recomputenode/src/utils.rshash_claim_commitmentalready implements the exact on-chain commitment and is not called on this pathChecked on
gc-v2. Not#435(Swap watcherfrom_scriptDoS). Not#428. Not#429/#430/#431. Not#21.Mechanism
CLI sends the user's requested address/amount to the LP, then trusts the returned
claimDatabytes. It never:outputAmount/outputScriptfrom the commitmenthash_claim_commitmentfrom--btc-address→ scriptPubKey + expected sats + nonce/confirmationsescrow.claimDataOn-chain claim only checks keccak of whatever script was committed. Timelock refund does not cover under-payment after a successful claim.
Impact
Per-user pegBTC theft against anyone who uses this CLI against a lying LP. Requires a malicious counterparty. Not an unauthenticated protocol path. Not BitVM3 kickoff/Take1/Take2.
Availability of a check that already exists in-repo (
hash_claim_commitment) is the defect.Suggested fix
In
parse_escrow_from_pay_invoice(or immediately after), recomputehash_claim_commitmentfrom the user's--btc-address/--amount/ nonce / confirmations / btcRelay and require== escrow.claimData. Also assertescrow.amount,escrow.token,escrow.claimHandler, and refund timelock match the request beforeswap_initialize. Print decodedoutputAmount+outputScriptand refuse to submit on mismatch.