[ANCHOR-1311]: One non-SAC contract payment permanently halts customer-fund crediting - #2020
Draft
amandagonsalves wants to merge 4 commits into
Draft
amandagonsalves wants to merge 4 commits into
amandagonsalves wants to merge 4 commits into
Conversation
* update invoke host function payment validation * remove sac to asset mapper from default payment listener * add listener error isolation in payment observers * update stellar rpc observer to use verified event data * fix horizon payment observer cursor persistence * update health check status for publisher errors
Contributor
There was a problem hiding this comment.
Pull request overview
This PR hardens Stellar payment observers against incorrect SAC resolution and malformed payment events.
Changes:
- Derives invoke-host-function assets and amounts from verified SAC activity.
- Improves cursor handling, listener isolation, and observer health reporting.
- Updates wiring and adds regression tests.
Reviewed changes
Copilot reviewed 11 out of 11 changed files in this pull request and generated 7 comments.
Show a summary per file
| File | Description |
|---|---|
| platform/src/test/kotlin/org/stellar/anchor/platform/observer/stellar/StellarRpcPaymentObserverTest.kt | Updated as part of this pull request. |
| platform/src/test/kotlin/org/stellar/anchor/platform/observer/stellar/StellarPaymentObserverTest.kt | Updated as part of this pull request. |
| platform/src/test/kotlin/org/stellar/anchor/platform/observer/stellar/SacToAssetMapperTest.kt | Updated as part of this pull request. |
| platform/src/test/kotlin/org/stellar/anchor/platform/observer/stellar/HorizonPaymentObserverTest.kt | Updated as part of this pull request. |
| platform/src/test/kotlin/org/stellar/anchor/platform/observer/stellar/DefaultPaymentListenerTest.kt | Updated as part of this pull request. |
| platform/src/main/java/org/stellar/anchor/platform/observer/stellar/StellarRpcPaymentObserver.java | Updated as part of this pull request. |
| platform/src/main/java/org/stellar/anchor/platform/observer/stellar/SacToAssetMapper.java | Updated as part of this pull request. |
| platform/src/main/java/org/stellar/anchor/platform/observer/stellar/HorizonPaymentObserver.java | Updated as part of this pull request. |
| platform/src/main/java/org/stellar/anchor/platform/observer/stellar/DefaultPaymentListener.java | Updated as part of this pull request. |
| platform/src/main/java/org/stellar/anchor/platform/observer/stellar/AbstractPaymentObserver.java | Updated as part of this pull request. |
| platform/src/main/java/org/stellar/anchor/platform/component/observer/PaymentObserverBeans.java | Updated as part of this pull request. |
Suppressed comments (2)
platform/src/main/java/org/stellar/anchor/platform/observer/stellar/AbstractPaymentObserver.java:152
PaymentListener.onReceiveddeclaresAnchorExceptionandIOException, so a listener can fail without throwingRuntimeException. Those exceptions bypass this isolation, abort the remaining listeners, and send the observer down its publisher-error path. CatchExceptionhere (or otherwise isolate the declared checked failures) to match the stated behavior.
} catch (RuntimeException rex) {
platform/src/main/java/org/stellar/anchor/platform/observer/stellar/StellarRpcPaymentObserver.java:427
- Using the verified event amount here still causes the forwarding-contract case to be dropped:
DefaultPaymentListener.processAndDispatchLedgerPayment()compares this value withledgerPayment.getAmount()(the top-level declared argument) and returns on mismatch. Thus a genuine SAC transfer is not credited at all when the router declares a different amount, contrary to the acceptance criterion. Invoke-host-function dispatch should use the verified event amount downstream instead of rejecting it against the attacker-controlled declaration.
.amount(BigInteger.valueOf(emittingAmount))
Note
Copilot is running an experiment and ran this review at Lite.
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
amandagonsalves
marked this pull request as ready for review
September 14, 2026 15:35
amandagonsalves
marked this pull request as draft
September 14, 2026 15:36
* add dynamic soroban invoke host function name parsing * add strict argument type and count validation for soroban invoke host functions * update payment amounts to biginteger * refactor payment listener error propagation * fix invoke host function payment processing with verified event amounts
ceciliaromao
approved these changes
Sep 16, 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
The payment observer resolved an invoke-host-function payment's asset from the top-level invoked
contract instead of the actual balance change. For any non-SAC contract that lookup returned null,
and reading it back was fatal, taking the whole observer offline and advancing the stored cursor
past the uncredited payment anyway.
Three classes derived or consumed that same wrong value (
DefaultPaymentListener,HorizonPaymentObserver,StellarRpcPaymentObserver), plus a set of reliability bugs turned onebad payment into a permanent, silent loss. Everything that used to throw now fails closed instead:
skip the operation, keep the observer running, keep the cursor where it was.
For RPC mode, the credited amount also came from the top-level call's own declared argument, not
the emitting contract's verified event, so a forwarding contract could declare any amount while
moving almost nothing. Amount is now taken from the same verified source as the asset.
Changes
SacToAssetMapper.java:getAssetFromSacfails closed on RPC errors instead of throwing.AbstractPaymentObserver.java:handleEventisolates one listener's exception per listener.HorizonPaymentObserver.java: asset comes from any matching SAC balance change, not[0]/entrypoint name; cursor advances only on success;fetchStreamingCursorresumes from the stored cursor;check()mapsPUBLISHER_ERRORto red.DefaultPaymentListener.java:validate()takes the asset from the event;SacToAssetMapperdependency removed.StellarRpcPaymentObserver.java: asset and amount come from the event-emitting contract, not the top-level invoked contract; skips instead of throwing on a non-SAC contract.PaymentObserverBeans.java:paymentListenerbean no longer wiresSacToAssetMapper.SacToAssetMapperTest.kt.Acceptance Criteria
check()reports red whilePUBLISHER_ERRORis active.Context
HackerOne #4001848
Testing
./gradlew :platform:test --tests "org.stellar.anchor.platform.observer.stellar.*" --rerun./gradlew :platform:test --rerun(all green)Documentation
N/A
Known limitations
from/tofor RPC-mode invoke-host-function payments still come from the top-level args, not theverified event. No new misattribution risk beyond the existing attacker-chosen memo, so left as is.