Skip to content

fix(ingest): verify SAC before recording contract balances - #695

Open
JiahuiWho wants to merge 2 commits into
mainfrom
fix/verify-sac-before-recording-balances
Open

fix(ingest): verify SAC before recording contract balances#695
JiahuiWho wants to merge 2 commits into
mainfrom
fix/verify-sac-before-recording-balances

Conversation

@JiahuiWho

@JiahuiWho JiahuiWho commented Aug 21, 2026

Copy link
Copy Markdown
Contributor

What

Record a SAC balance only when its contract is confirmed as a SAC via its instance entry(i.e. it exists in contract_tokens with type='SAC').

  • Live (token_ingestion.go): gate the balance upsert on a new ContractModel.GetExistingSACByID lookup.
  • Checkpoint (checkpoint.go): stop creating a contract_tokens SAC row from balance shape. Stream balances through the batch as before, then in finalize delete any whose contract is not a confirmed SAC via SACBalanceModel.DeleteUnverified. This runs before COMMIT, so the deferred fk_contract_token holds and no unconfirmed contract is recorded.

Why

SAC balances were recorded from the shape of a contract-data entry alone, without confirming that the writing contract is actually a Stellar Asset Contract. Any contract can write that shape into its own storage, so balances got attached to arbitrary contracts.

Two consequences:

  • Live ingestion (availability): the balance row references a contract_id with no contract_tokens parent, so the deferred fk_contract_token constraint fires at COMMIT (SQLSTATE 23503). That error is classified as permanent (non-retryable) and shares the transaction with the ingestion cursor update, so the ledger rolls back without advancing the cursor and re-fails identically on restart.
  • Checkpoint bootstrap (integrity): the balance path additionally created a contract_tokens row with type='SAC' from the shape, classifying an arbitrary contract as a SAC.

Known limitations

A balance cannot be authenticated from its own entry, so a balance whose SAC instance entry isn't present(i.e. a dormant SAC whose instance was state-archived at bootstrap time) is now dropped rather than trusted. This is bounded and self-healing: such a SAC is inactive, and the next time it's used its instance must be restored, which live ingestion re-ingests and gates the balances back in.

Issue that this PR addresses

https://hackerone.com/reports/3923230

Checklist

PR Structure

  • It is not possible to break this PR down into smaller PRs.
  • This PR does not mix refactoring changes with feature changes.
  • This PR's title starts with name of package that is most changed in the PR, or all if the changes are broad or impact many packages.

Thoroughness

  • This PR adds tests for the new functionality or fixes.
  • All updated queries have been tested (refer to this check if the data set returned by the updated query is expected to be same as the original one).

Release

  • This is not a breaking change.
  • This is ready to be tested in development.
  • The new functionality is gated with a feature flag if this is not ready for production.

@JiahuiWho
JiahuiWho requested review from aditya1702 and a balanced review from Copilot August 21, 2026 21:34
@JiahuiWho
JiahuiWho marked this pull request as ready for review August 21, 2026 21:36

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Gates live and checkpoint SAC balance ingestion on verified SAC contract instances.

Changes:

  • Adds SAC contract verification for live ingestion.
  • Defers and filters checkpoint SAC balances.
  • Adds model support, dependency wiring, mocks, and regression tests.

Reviewed changes

Copilot reviewed 7 out of 7 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
internal/services/token_ingestion.go Gates live SAC balance writes.
internal/services/token_ingestion_test.go Tests verified and unverified balances.
internal/services/checkpoint.go Defers and filters checkpoint balances.
internal/services/checkpoint_test.go Tests checkpoint verification behavior.
internal/ingest/ingest.go Wires the contract model.
internal/data/mocks.go Extends the contract model mock.
internal/data/contract_tokens.go Adds the SAC lookup query.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread internal/services/checkpoint.go Outdated
Comment thread internal/data/contract_tokens.go
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