Add refund address guards and fix sweep security

- Guard all refund processing with refund_address checks to avoid unnecessary loops
- Replace dangerous sweep_all with exact amount transfers
- Use transfer() with specific amounts instead of sweeping everything
- Prevents stealing funds from other concurrent payments in same account
- Only process refunds when customer has configured a refund address

Performance: Skip expensive refund calculations when no refund address exists
Security: Each payment only touches its own calculated amounts (91% + 9%)
This commit is contained in:
Russell Ballestrini 2025-09-23 18:40:10 -04:00
parent 326c79fbda
commit 663c0e9546

View file

@ -600,7 +600,7 @@ def process_payment(
except Exception as e:
logger.error(f"Failed to process full refund for expired payment {crypto_payment.id}: {e}")
elif payment_rescue:
elif payment_rescue and crypto_payment.refund_address:
# Not out of stock - do normal expired refund with restocking fee
coin_config = get_coin_config(crypto_payment.coin_type)
received_crypto = Decimal(total_recv) / coin_config["atomic_units"]
@ -620,42 +620,57 @@ def process_payment(
crypto_payment.refund_amount = int(refund_details["refund_amount"] * coin_config["atomic_units"])
crypto_payment.refund_tx_hash = result["tx_hash"]
crypto_payment.refund_reason = refund_details["reason"]
crypto_payment.status = CryptoPayment.STATUS_EXPIRED_REFUNDED
# Now sweep the restocking fee to shop's wallet
# Commit the refund first
env_request.dbsession.add(crypto_payment)
env_request.dbsession.flush()
# Now sweep the remaining restocking fee to shop's wallet (AFTER refund)
fee_amount = int(refund_details["fee_amount"] * coin_config["atomic_units"])
if fee_amount > 0 and crypto_payment.shop_sweep_to_address:
try:
# Small delay to ensure refund tx is processed
import time
time.sleep(2)
if crypto_payment.coin_type == "XMR":
sweep_result = client._call("sweep_single", {
# Send exact restocking fee amount (9%)
sweep_result = client._call("transfer", {
"account_index": crypto_payment.account_index,
"address": crypto_payment.shop_sweep_to_address,
"amount": fee_amount,
"destinations": [{
"address": crypto_payment.shop_sweep_to_address,
"amount": fee_amount
}],
"priority": 1,
"get_tx_hex": True,
"do_not_relay": False,
})
fee_tx_hash = sweep_result.get("tx_hash_list", [None])[0]
fee_tx_hash = sweep_result.get("tx_hash")
actual_swept = fee_amount
elif crypto_payment.coin_type == "DOGE":
# Send exact restocking fee amount
fee_crypto_amount = float(Decimal(fee_amount) / coin_config["atomic_units"])
fee_tx_hash = client._call("sendtoaddress", [
crypto_payment.shop_sweep_to_address,
float(Decimal(fee_amount) / coin_config["atomic_units"])
fee_crypto_amount
])
actual_swept = fee_amount
else:
fee_tx_hash = None
actual_swept = 0
if fee_tx_hash:
crypto_payment.swept_amount = fee_amount
if fee_tx_hash and actual_swept > 0:
crypto_payment.swept_amount = actual_swept
crypto_payment.swept_tx_hash = fee_tx_hash
crypto_payment.swept_timestamp = now_timestamp()
logger.info(
f"Restocking fee swept for payment {crypto_payment.id}: "
f"{Decimal(fee_amount) / coin_config['atomic_units']} {crypto_payment.coin_type} "
f"{Decimal(actual_swept) / coin_config['atomic_units']} {crypto_payment.coin_type} "
f"TX: {fee_tx_hash}"
)
except Exception as e:
logger.error(f"Failed to sweep restocking fee for payment {crypto_payment.id}: {e}")
crypto_payment.status = CryptoPayment.STATUS_EXPIRED_REFUNDED
else:
logger.error(
f"Refund failed for expired payment {crypto_payment.id}: {result['error']}"
@ -738,6 +753,7 @@ def process_payment(
# Check for overpayment refund AFTER confirming the order (applies to both new and stuck payments)
if (
payment_rescue
and crypto_payment.refund_address
and crypto_payment.received_amount > crypto_payment.expected_amount
and crypto_payment.invoice
and crypto_payment.invoice.user
@ -781,6 +797,7 @@ def process_payment(
# Check for underpayment rescue when we have enough confirmations
if (
payment_rescue
and crypto_payment.refund_address
and min_confs >= int(crypto_payment.confirmations_required)
and crypto_payment.invoice
and crypto_payment.invoice.user