Skip to content

[ANCHOR-1407]: Enhance underpayment handling and transaction recording logic - #2091

Merged
ceciliaromao merged 10 commits into
developfrom
fix/anchor-1407
Nov 13, 2026
Merged

ceciliaromao merged 10 commits into
developfrom
fix/anchor-1407

Conversation

@ceciliaromao

Copy link
Copy Markdown
Collaborator

Description

  • When DefaultPaymentListener rejects a payment because its on-chain amount is below amount_expected, it no longer drops it silently. It now:
    • appends the Stellar transaction to the transaction's stellar_transactions and saves it;
    • publishes one transaction_status_changed event (no new event type), so the business server sees the payment;
    • leaves status, amount_in and the other amounts untouched and does not call the Platform API, so the underpayment is still not credited.
  • Covers SEP-31, SEP-24 withdrawal and SEP-6 withdrawal / withdrawal-exchange.
  • A failure to save (DataAccessException, TransactionException) or to publish (AnchorException or an unchecked exception) throws IOException, so the observer cursor is held and the payment is retried instead of dropped again.
  • A redelivered payment already present in stellar_transactions is not appended twice, but its event is sent again.
  • PaymentHelper.addStellarTransaction now merges by Stellar transaction hash instead of appending blindly, so crediting a recorded payment later through notify_onchain_funds_received (hash + real amount) leaves a single entry. It also stops failing on a null list that was an immutable List.of.
  • DefaultPaymentListener now receives EventService and SacToAssetMapper (wired in PaymentObserverBeans).

Context

  • HackerOne #4109384: an underpaid payment (e.g. 99 sent for 100 expected, typical when an exchange deducts a withdrawal fee) was logged and counted, but checkAssetAmountSufficient returned false with no exception, so the observer cursor advanced for good. The funds sat in the distribution account while the transaction stayed in pending_sender / pending_user_transfer_start with no record and no signal to the anchor.
  • The sufficiency rule itself is correct and unchanged. The missing piece was somewhere for the rejected payment to go, so an operator can credit it, ask for the rest, or refund it. Chosen approach: record + event, with no automatic credit, to avoid reopening the "underpayment advances to payout" class (#3710899, #3681336, #3575732).

Testing

  • ./gradlew test
  • ./gradlew :platform:spotlessCheck :platform:test at HEAD: 1453 tests, 0 failures.
  • New tests in DefaultPaymentListenerTest, PaymentHelperTest and NotifyOnchainFundsReceivedHandlerTest cover: record + event for SEP-31 / SEP-24 / SEP-6 (both kinds), status and amounts unchanged, no Platform API call, save and publish failures holding the cursor, optimistic-lock conflict, redelivery, events disabled, muxed fallback, path payment, one stroop below, a second underpayment (recorded separately, not summed), and the later credit leaving one entry.
  • An independent verification pass injected 3 mutations (event id, redelivery guard, merge of a repeated hash); the weak tests it found were tightened and all 3 mutations are now killed.
  • Not run: extended-tests (e2e against a live stack).

Documentation

N/A for stellar-docs in this PR. Business servers will now receive a transaction_status_changed event with an unchanged status when an underpayment is recorded; consider a docs note on that behavior and on crediting through notify_onchain_funds_received.

  • Attach stellar-docs pull request, documenting new feature
  • If it's an urgent feature request, please create a ticket and attach ticket number to this PR

Known limitations

  • KafkaSession.publish logs and swallows every exception, so with Kafka a failed event delivery never reaches the listener: the saved stellar_transactions entry is the guaranteed signal and the event is best effort. Changing that is out of scope and affects every event.
  • Several underpayments for the same transaction are recorded separately and not summed; the operator decides through notify_onchain_funds_received.
  • Not covered here: other silent drops of a routed payment (SEP-31 asset mismatch, ambiguous routing) and a backfill for payments dropped before this fix ships.

@ceciliaromao ceciliaromao self-assigned this Nov 2, 2026
@ceciliaromao
ceciliaromao merged commit 35fc928 into develop Nov 13, 2026
35 of 38 checks passed
@ceciliaromao
ceciliaromao deleted the fix/anchor-1407 branch November 13, 2026 19:15
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