Skip to content

ingest: LedgerTransaction.NewMaxFee() truncates int64 fee-bump fee to uint32, causing silent data corruption #5931

@hunterpack

Description

@hunterpack

What version are you using?

go list -m github.com/stellar/go-stellar-sdk

Bug is present in current main branch at ingest/ledger_transaction.go:739.


What did you do?

Called LedgerTransaction.NewMaxFee() on a fee-bump transaction where the outer fee (FeeBumpTransaction.Fee) exceeded 4,294,967,295 stroops (~429.5 XLM):

tx, _ := reader.Read()
fee, ok := tx.NewMaxFee()
// fee is silently truncated for any fee-bump tx with fee > 429.5 XLM

Fee-bump transactions with fees exceeding this threshold are realistic for resource-intensive Soroban smart contract executions or during network congestion. The Stellar XDR spec deliberately defines FeeBumpTransaction.Fee as Int64 to accommodate values above the Uint32 range.


What did you expect to see?

NewMaxFee() should return the full correct int64 value of the fee-bump outer fee, consistent with:

  • The XDR spec: FeeBumpTransaction.Fee is Int64
  • The underlying accessor: Envelope.FeeBumpFee() already returns int64

For example, a fee of 5,000,000,000 stroops (500 XLM) should be returned as 5000000000.


What did you see instead?

NewMaxFee() silently truncates the int64 fee to uint32, discarding the upper 32 bits and returning a completely wrong value with no error.

Vulnerable code — ingest/ledger_transaction.go:739:

func (t *LedgerTransaction) NewMaxFee() (uint32, bool) {
    if !t.Envelope.IsFeeBump() {
        return 0, false
    }
    return uint32(t.Envelope.FeeBumpFee()), true  // ← BUG: int64 truncated to uint32
}

Example of data corruption: A fee of 5,000,000,000 stroops becomes 705,032,704 (~70.5 XLM instead of ~500 XLM). The math: 5,000,000,000 mod 4,294,967,296 = 705,032,704.

Root cause: MaxFee() on a regular inner transaction correctly returns uint32 because the inner transaction fee uses XDR Uint32. NewMaxFee() was modelled after it without accounting for the fact that the fee-bump outer fee uses XDR Int64. The wrong type and cast were copied without adjustment.

Proposed fix:

func (t *LedgerTransaction) NewMaxFee() (int64, bool) {
    if !t.Envelope.IsFeeBump() {
        return 0, false
    }
    return t.Envelope.FeeBumpFee(), true
}

This is a breaking change to the public function signature. All callers must update their type handling from uint32 to int64.

Known affected downstream consumer: stellar/stellar-etl has the same truncation bug independently at internal/transform/transaction.go:294 and internal/transform/schema.go:63 — that fix should be coordinated with this one.

Impact: Any application using LedgerTransaction.NewMaxFee() from the ingest SDK silently receives an incorrect fee value for any fee-bump transaction with a fee exceeding ~429.5 XLM. This affects all downstream analytics, reconciliation, or compliance systems built on this function.

Metadata

Metadata

Assignees

Type

No type

Projects

Status

Done

Relationships

None yet

Development

No branches or pull requests

Issue actions