Skip to content

[ANCHOR-1336]: Record funds-received only for a payment that belongs to the transaction - #2098

Open
ceciliaromao wants to merge 8 commits into
developfrom
fix/anchor-1336
Open

ceciliaromao wants to merge 8 commits into
developfrom
fix/anchor-1336

Conversation

@ceciliaromao

Copy link
Copy Markdown
Collaborator

Description

  • notify_onchain_funds_received now records a ledger hash as the received payment only when the ledger transaction holds a payment to the transaction's receiving account (withdraw_anchor_account for SEP-6/24 withdrawals, to_account for SEP-31), with the stored memo, in the amount_in asset, for an amount above zero. Otherwise it throws InvalidParamsException naming the missing element (destination, memo, asset or amount) and the hash, and changes, saves and publishes nothing. The amount is not compared with amount_expected, so partial payments keep working.
  • notify_onchain_funds_sent (SEP-6/24 deposit payout) applies the same check to the recipient (to_account) and the amount_out asset. The memo is not compared there: payouts do not have to carry the deposit's memo (the reference server's payout carries none).
  • notify_transaction_on_hold no longer sets transferReceivedAt. A hold only changes the status, so an unpaid held transaction can be expired again and its payment can still be recorded.
  • New LedgerPaymentMatcher (platform/utils) holds the check: destination, memo, asset, amount, in that order; muxed destinations are matched by their id for any sender, as the payment observer does; a hash memo is accepted in both the hex and base64 forms the observer looks up. The observer itself is unchanged.
  • Test infrastructure: the platform API integration flows now pay each transaction's own destination (one payment per transaction) instead of reusing one shared testnet payment.

Context

  • HackerOne 4036789
  • The RPC trusted any existing hash as proof of payment, while the observer checks destination, memo and asset before calling the same RPC. The on-hold side effect made the funds gate read as passed without a payment. Both are internal inconsistencies of supported RPCs and were confirmed in triage (ANCHOR-1336). The Platform API's auth default is hardening and is not part of this change.

Testing

  • ./gradlew test
  • Unit: ./gradlew :platform:test --tests "org.stellar.anchor.platform.utils.LedgerPaymentMatcherTest" --tests "org.stellar.anchor.platform.rpc.NotifyOnchainFundsReceivedHandlerTest" --tests "org.stellar.anchor.platform.rpc.NotifyOnchainFundsSentHandlerTest" --tests "org.stellar.anchor.platform.rpc.NotifyTransactionOnHoldHandlerTest" --tests "org.stellar.anchor.platform.rpc.OnHoldFundsGateTest"
    New tests cover every rejection cause per protocol, the amount boundary (0 rejected, 1 stroop accepted), several payments in one transaction, muxed destinations, hash memos, an observer-parity test that runs the real DefaultPaymentListener and then the RPC, and the on-hold chain.
  • Integration (live stack, testnet): ./gradlew :essential-tests:test --tests "*Sep24PlatformApiTests*" --tests "*Sep6PlatformApiTests*" --tests "*Sep31PlatformApiTests*"
    Run locally: SEP-24 9/9, SEP-6 9/10. Not verified locally: the SEP-31 suite (the reference server's creation event arrived about 30 s late and the testnet RPC rejected the observer's polls, so the transaction never reached pending_sender within 60 s) and SEP-6 "deposit complete full with trust flow" (it fails on its first response with the new flow runner switched off too: the reference server completes the deposit before the test's first call). Please rely on CI for these two.
    The fixtures now pay destination_account with the transaction's memo. The testnet client wallet account (GDJLBYYK…) needs a trustline to Circle's USDC:GBBD47IF6LWK7P7MDEVSCWR7DPUWV3NY3DTQEVFL4NAT4AQH3ZLLFLA5 for the withdrawal flows to be paid.
  • Manual: N/A (the reporter's PoC was static analysis; its two JUnit tests are not in the repo).

Documentation

  • N/A. No configuration, API shape or documented behaviour for integrators changes other than the new rejection of an unrelated hash.

Known limitations

  • The same hash can still be recorded on a second transaction (not part of the report; follow-up candidate).
  • The deposit payout is not matched on memo; the amount is not compared with amount_expected (partial payments are intended, see report 3575732).
  • The observer routes a memo by value only, while the RPC also compares the memo type: a text memo whose digits equal a stored id memo is forwarded by the observer and refused by the RPC (covered by a test, not changed here).
  • Comment typo at anchor-config-default-values.yaml:119 from the report is not touched.

@ceciliaromao ceciliaromao self-assigned this Nov 9, 2026
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.

1 participant