[ANCHOR-1225]: Strict-send sendAmount mis-credited as received USDC lets any wallet user drain a full withdrawal payout for ~$0 on-chain#1967
Merged
Conversation
* add operation result parsing in stellar and horizon clients * refactor LedgerClientHelper to extract actual received amount for path_payment_strict_send from operation result * add a critical security check in DefaultPaymentListener to compare independently computed event amounts and assets with ledger-parsed data * fix potential amount spoofing vulnerability for path payments
Contributor
There was a problem hiding this comment.
Pull request overview
This PR addresses a critical accounting flaw for PATH_PAYMENT_STRICT_SEND by ensuring the credited amount reflects what the destination actually received (from the operation result), and adds an observer-side cross-check to refuse processing when independently-computed event data disagrees with ledger-parsed data.
Changes:
- Parse and thread per-operation results from transaction results (fee-bump aware) so
PATH_PAYMENT_STRICT_SENDuses the actual received amount fromPathPaymentStrictSendResult.success.last.amount. - Add an event-vs-ledger amount/asset cross-check in
DefaultPaymentListenerand extend unit tests to cover strict-send exploit scenarios. - Add a new metric enum entry for observer amount/asset mismatch.
Reviewed changes
Copilot reviewed 7 out of 7 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
| platform/src/test/kotlin/org/stellar/anchor/platform/observer/stellar/DefaultPaymentListenerTest.kt | Adds strict-send fixtures/tests for observer mismatch blocking and ensures sep11Asset is set in test events. |
| platform/src/main/java/org/stellar/anchor/platform/service/AnchorMetrics.java | Adds a metric constant for observer amount/asset mismatches. |
| platform/src/main/java/org/stellar/anchor/platform/observer/stellar/DefaultPaymentListener.java | Adds event-vs-ledger amount/asset cross-check before dispatching a received payment. |
| core/src/test/kotlin/org/stellar/anchor/ledger/LedgerClientHelperTest.kt | Updates convert signature usage and adds tests for strict-send result amount parsing and operation-result parsing behavior. |
| core/src/main/java/org/stellar/anchor/ledger/StellarRpc.java | Parses resultXdr and threads operation results into getLedgerOperations. |
| core/src/main/java/org/stellar/anchor/ledger/LedgerClientHelper.java | Adds strict-send received-amount extraction from op results, adds parseOperationResults, and enforces ops/results length matching. |
| core/src/main/java/org/stellar/anchor/ledger/Horizon.java | Parses resultXdr and threads operation results into getLedgerOperations. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
* refactor payment amount validation to reject payments smaller than expected * add new metric for insufficient payment amounts * update payment processing to halt when amount is insufficient * add tests for payment rejection and metric incrementation
* refactor `extractStrictSendReceivedAmount` to clarify path payment strict send result parsing * add explicit null checks for nested `pathpaymentstrictsendresult` objects * add tests to validate `ledgerexception` when critical result components are missing
JiahuiWho
approved these changes
Jul 10, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Description
For
PATH_PAYMENT_STRICT_SENDoperations,LedgerClientHelper.convertpaired the correct destination asset (destAsset) with the wrong amount (sendAmount- the source's debit, not what the destination received). The real received amount only exists in the operation's execution result (PathPaymentStrictSendResult.success.last.amount), never parsed.An attacker could send a strict-send payment where
sendAmount(in a worthless self-issued asset) met the expected amount while the anchor actually received dust of the real asset - the mismatch guard compared the wrong number and stayed silent, and automated-payout deployments released the full off-chain value. Both observer backends were affected; RPC had no correct value anywhere.For quoted flows (SEP-31,
WITHDRAWAL_EXCHANGE),amount_outis fixed at quote time, and neither the RPC handlers nor the reference server's payout trigger compareamount_intoamount_expectedbefore paying out - they only check that some payment arrived. So the mismatch guard also had to become blocking.Changes
LedgerClientHelper.convert: takes anOperationResult; forPATH_PAYMENT_STRICT_SEND, reads the amount fromPathPaymentStrictSendResult.success.last.amountinstead ofsendAmount, failing closed withLedgerExceptionif the result is missing/non-success/wrong-type. Other operation types unaffected.LedgerClientHelper: addedparseOperationResultsto extract per-op results from aTransactionResult(fee-bump aware);getLedgerOperationstakes the result array and throws on an operations/results length mismatch.Horizon.getTransaction/StellarRpc.fromGetTransactionResponse: both now parseparseResultXdr()and thread the results through, fixing both observer backends.DefaultPaymentListener.processAndDispatchLedgerPayment: added a cross-check (aftervalidate()) comparing the independently-computedPaymentTransferEventamount/asset against the ledger-parsed values; refuses to dispatch on disagreement.DefaultPaymentListener.checkAndWarnAssetAmountMismatch→ renamedcheckAssetAmountSufficient, returnsboolean. The threehandleSepXXTransactionmethods skip notifying and all metrics when it'sfalse. Only the amount check blocks - the asset-name check stays advisory, since it's also reached forDEPOSIT-kind rows where it's expected to "mismatch" (off-chain vs on-chain asset); those rows are unreachable here in practice, but there's no need to bet on that.AnchorMetrics: addedPAYMENT_OBSERVER_AMOUNT_ASSET_MISMATCHandPAYMENT_OBSERVER_AMOUNT_INSUFFICIENT.LedgerClientHelperTest: updated allconvert()call sites for the new signature; added tests for result-amount-not-sendAmount, throw-on-bad-result,parseOperationResults, and the length-mismatch guard.DefaultPaymentListenerTest: added a strict-send fixture with independent event/ledger amount+asset; tests for the happy path, the exploit scenario, and the asset-mismatch case. FixedcreateTestTransferEvent()to setsep11Assetso the new gate doesn't regressPAYMENT-path tests. RenamedcheckAndWarnAssetAmountMismatchcall sites; added tests for the insufficient-amount rejection and its metric.Acceptance Criteria
PATH_PAYMENT_STRICT_SENDcredits the result amount, notsendAmount; both observer backends agree.sendAmountbut dust actual receipt is not credited and does not advance any SEP-6/24/31 transaction.convertthrows for a missing/non-success/mismatched-type result rather than defaulting to any amount.DefaultPaymentListenerrefuses to dispatch (and increments the metric) on event-vs-ledger disagreement, or on insufficient amount.PAYMENT/PATH_PAYMENT_STRICT_RECEIVEhandling and test suites are unaffected.Context
#3810844
Testing
./gradlew :core:test --tests "org.stellar.anchor.ledger.LedgerClientHelperTest"./gradlew :platform:test --tests "org.stellar.anchor.platform.observer.stellar.DefaultPaymentListenerTest"Documentation
N/A
Known limitations
This PR closes the gap for the observer-driven path (the reported attack surface and default deployment mode); direct/external RPC callers are not addressed here.