Skip to content

Commit c395377

Browse files
sameh-faroukclaude
andauthored
fix(bridge): correct Stellar deposit asset validation (#1094)
Three correctness fixes in the Stellar->TFChain deposit path (processTransaction) and balance reporting (StatBridgeAccount): 1. Credited-effect asset check used && instead of ||, so a credit was only skipped when BOTH the asset code and issuer were wrong. A credit with the right code but a forged/wrong issuer (or vice-versa) passed the gate and could be minted. Now requires both to match. 2. The per-operation loop did `return nil, nil` on the first non-payment operation, abandoning the entire transaction and silently dropping any legitimate payment operations after it (lost deposits / missing mints). Now skips non-payment ops individually with `continue`. 3. Added operation-level asset validation: even after the effect check, each payment amount must itself be TFT, otherwise a non-TFT payment to the bridge in the same transaction would be summed and minted as TFT. Non-TFT payments are now skipped and logged as an alert. 4. StatBridgeAccount matched the TFT balance with || (code OR issuer), which could return an unrelated asset's balance; now matches on both. Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 6375d96 commit c395377

1 file changed

Lines changed: 32 additions & 3 deletions

File tree

bridge/tfchain_bridge/pkg/stellar/stellar.go

Lines changed: 32 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -512,7 +512,11 @@ func (w *StellarWallet) processTransaction(tx hProtocol.Transaction) ([]MintEven
512512
}
513513

514514
creditedEffect := effect.(horizoneffects.AccountCredited)
515-
if creditedEffect.Code != asset[0] && creditedEffect.Issuer != asset[1] {
515+
// Skip the effect unless BOTH the asset code and issuer match the
516+
// bridge's TFT asset. Using && here meant a credit was only skipped
517+
// when both differed, so a credit with the right code but a wrong
518+
// issuer (or vice-versa) slipped through and could be minted.
519+
if creditedEffect.Code != asset[0] || creditedEffect.Issuer != asset[1] {
516520
continue
517521
}
518522

@@ -523,15 +527,37 @@ func (w *StellarWallet) processTransaction(tx hProtocol.Transaction) ([]MintEven
523527

524528
senders := make(map[string]*big.Int)
525529
for _, op := range ops.Embedded.Records {
530+
// Skip non-payment operations individually. Previously this
531+
// returned from the whole function on the first non-payment op,
532+
// silently dropping any legitimate payment ops in the same
533+
// transaction (lost deposits / missing mints).
526534
if op.GetType() != "payment" {
527-
return nil, nil
535+
continue
528536
}
529537

530538
PaymentOperation := op.(operations.Payment)
531539
if PaymentOperation.To != w.config.StellarBridgeAccount || PaymentOperation.From == w.config.StellarBridgeAccount {
532540
continue
533541
}
534542

543+
// Validate the payment asset at the operation level too. The
544+
// account_credited effect check above gates entry into this loop,
545+
// but the per-payment amount must itself be TFT — otherwise a
546+
// non-TFT payment to the bridge in the same transaction would be
547+
// summed and minted as TFT.
548+
if PaymentOperation.Code != asset[0] || PaymentOperation.Issuer != asset[1] {
549+
logger.Warn().
550+
Str("event_action", "non_tft_payment_rejected").
551+
Str("event_kind", "alert").
552+
Str("from", PaymentOperation.From).
553+
Str("asset_code", PaymentOperation.Code).
554+
Str("asset_issuer", PaymentOperation.Issuer).
555+
Str("amount", PaymentOperation.Amount).
556+
Str("tx_hash", PaymentOperation.TransactionHash).
557+
Msg("non-TFT payment to bridge detected — skipping")
558+
continue
559+
}
560+
535561
parsedAmount, err := amount.ParseInt64(PaymentOperation.Amount)
536562
if err != nil {
537563
continue
@@ -677,7 +703,10 @@ func (w *StellarWallet) StatBridgeAccount() (string, error) {
677703
asset := w.getAssetCodeAndIssuer()
678704

679705
for _, balance := range acc.Balances {
680-
if balance.Code == asset[0] || balance.Issuer == asset[1] {
706+
// Match the TFT balance on BOTH code and issuer. Using || could
707+
// return an unrelated asset's balance that happened to share either
708+
// the code or the issuer.
709+
if balance.Code == asset[0] && balance.Issuer == asset[1] {
681710
return balance.Balance, nil
682711
}
683712
}

0 commit comments

Comments
 (0)