From 009c7f171c9cee0225cd1710b5dbfc6aa5732a65 Mon Sep 17 00:00:00 2001 From: Sameh Abouel-saad Date: Mon, 1 Jun 2026 21:15:04 +0300 Subject: [PATCH] fix(bridge): correct Stellar deposit asset validation 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) --- bridge/tfchain_bridge/pkg/stellar/stellar.go | 35 ++++++++++++++++++-- 1 file changed, 32 insertions(+), 3 deletions(-) diff --git a/bridge/tfchain_bridge/pkg/stellar/stellar.go b/bridge/tfchain_bridge/pkg/stellar/stellar.go index 7f923744e..fb5f74e65 100644 --- a/bridge/tfchain_bridge/pkg/stellar/stellar.go +++ b/bridge/tfchain_bridge/pkg/stellar/stellar.go @@ -512,7 +512,11 @@ func (w *StellarWallet) processTransaction(tx hProtocol.Transaction) ([]MintEven } creditedEffect := effect.(horizoneffects.AccountCredited) - if creditedEffect.Code != asset[0] && creditedEffect.Issuer != asset[1] { + // Skip the effect unless BOTH the asset code and issuer match the + // bridge's TFT asset. Using && here meant a credit was only skipped + // when both differed, so a credit with the right code but a wrong + // issuer (or vice-versa) slipped through and could be minted. + if creditedEffect.Code != asset[0] || creditedEffect.Issuer != asset[1] { continue } @@ -523,8 +527,12 @@ func (w *StellarWallet) processTransaction(tx hProtocol.Transaction) ([]MintEven senders := make(map[string]*big.Int) for _, op := range ops.Embedded.Records { + // Skip non-payment operations individually. Previously this + // returned from the whole function on the first non-payment op, + // silently dropping any legitimate payment ops in the same + // transaction (lost deposits / missing mints). if op.GetType() != "payment" { - return nil, nil + continue } PaymentOperation := op.(operations.Payment) @@ -532,6 +540,24 @@ func (w *StellarWallet) processTransaction(tx hProtocol.Transaction) ([]MintEven continue } + // Validate the payment asset at the operation level too. The + // account_credited effect check above gates entry into this loop, + // but the per-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. + if PaymentOperation.Code != asset[0] || PaymentOperation.Issuer != asset[1] { + logger.Warn(). + Str("event_action", "non_tft_payment_rejected"). + Str("event_kind", "alert"). + Str("from", PaymentOperation.From). + Str("asset_code", PaymentOperation.Code). + Str("asset_issuer", PaymentOperation.Issuer). + Str("amount", PaymentOperation.Amount). + Str("tx_hash", PaymentOperation.TransactionHash). + Msg("non-TFT payment to bridge detected — skipping") + continue + } + parsedAmount, err := amount.ParseInt64(PaymentOperation.Amount) if err != nil { continue @@ -677,7 +703,10 @@ func (w *StellarWallet) StatBridgeAccount() (string, error) { asset := w.getAssetCodeAndIssuer() for _, balance := range acc.Balances { - if balance.Code == asset[0] || balance.Issuer == asset[1] { + // Match the TFT balance on BOTH code and issuer. Using || could + // return an unrelated asset's balance that happened to share either + // the code or the issuer. + if balance.Code == asset[0] && balance.Issuer == asset[1] { return balance.Balance, nil } }