In PaymentGate::send, the fee transfer and net transfer are two separate token.transfer calls:
if fee > 0 { token.transfer(&sender, &fee_collector, &fee); } token.transfer(&sender, &recipient, &net);
Soroban's execution model is single-threaded so classical reentrancy isn't possible, but the two-transfer pattern means a failure on the second transfer (e.g. recipient's trustline issue) leaves the fee already paid. The sender loses the fee without the payment completing.
Fix: Investigate whether Stellar's SAC (Stellar Asset Contract) can batch these into an atomic operation, or add a try/catch equivalent with a refund path, or document the accepted behaviour explicitly.
Acceptance criteria:
In PaymentGate::send, the fee transfer and net transfer are two separate token.transfer calls:
if fee > 0 { token.transfer(&sender, &fee_collector, &fee); } token.transfer(&sender, &recipient, &net);Soroban's execution model is single-threaded so classical reentrancy isn't possible, but the two-transfer pattern means a failure on the second transfer (e.g. recipient's trustline issue) leaves the fee already paid. The sender loses the fee without the payment completing.
Fix: Investigate whether Stellar's SAC (Stellar Asset Contract) can batch these into an atomic operation, or add a try/catch equivalent with a refund path, or document the accepted behaviour explicitly.
Acceptance criteria:
Add a test case covering "fee transferred but net transfer fails" scenario
Either implement atomic handling or add a clear code comment explaining the accepted risk