From 4b4701f5b962393e0940ac6c2e0aa9bf64dfec03 Mon Sep 17 00:00:00 2001 From: line-6000 <154492442+line-6000@users.noreply.github.com> Date: Fri, 22 May 2026 15:13:09 +0800 Subject: [PATCH] fix: skip stale EVM ERC20 transfers before order creation --- src/model/service/task_service.go | 4 ++ src/model/service/task_service_test.go | 65 ++++++++++++++++++++++++++ 2 files changed, 69 insertions(+) diff --git a/src/model/service/task_service.go b/src/model/service/task_service.go index 8684307..d0d1cd0 100644 --- a/src/model/service/task_service.go +++ b/src/model/service/task_service.go @@ -257,6 +257,10 @@ func TryProcessEvmERC20Transfer(chainNetwork string, contract common.Address, to log.Sugar.Warnf("[%s-%s][%s] skip trade_id=%s token mismatch order=%s", net, tokenSym, walletAddr, tradeID, order.Token) return } + if blockTsMs > 0 && blockTsMs < order.CreatedAt.TimestampMilli() { + log.Sugar.Warnf("[%s-%s][%s] skip tx %s because block time %d is before order create time %d", net, tokenSym, walletAddr, txHash, blockTsMs, order.CreatedAt.TimestampMilli()) + return + } req := &request.OrderProcessingRequest{ ReceiveAddress: walletAddr, diff --git a/src/model/service/task_service_test.go b/src/model/service/task_service_test.go index 5d0e80d..ab5e4de 100644 --- a/src/model/service/task_service_test.go +++ b/src/model/service/task_service_test.go @@ -144,3 +144,68 @@ func TestTryProcessEvmERC20TransferUsesChainTokenContract(t *testing.T) { t.Fatalf("runtime lock still exists for trade_id=%q", lockTradeID) } } + +func TestTryProcessEvmERC20TransferSkipsTransfersBeforeOrderCreation(t *testing.T) { + cleanup := testutil.SetupTestDatabases(t) + defer cleanup() + + const ( + tradeID = "T202605110002" + orderID = "ORD202605110002" + tokenSym = "USDT" + amount = 25.5 + ) + contract := common.HexToAddress("0x3333333333333333333333333333333333333333") + receiveAddress := common.HexToAddress("0x4444444444444444444444444444444444444444") + + if err := dao.Mdb.Create(&mdb.ChainToken{ + Network: mdb.NetworkEthereum, + Symbol: tokenSym, + ContractAddress: contract.Hex(), + Decimals: 6, + Enabled: true, + }).Error; err != nil { + t.Fatalf("seed chain token: %v", err) + } + + order := &mdb.Orders{ + TradeId: tradeID, + OrderId: orderID, + Amount: 100, + Currency: "CNY", + ActualAmount: amount, + Token: tokenSym, + Network: mdb.NetworkEthereum, + ReceiveAddress: strings.ToLower(receiveAddress.Hex()), + Status: mdb.StatusWaitPay, + } + if err := dao.Mdb.Create(order).Error; err != nil { + t.Fatalf("seed order: %v", err) + } + if err := data.LockTransaction(mdb.NetworkEthereum, order.ReceiveAddress, order.Token, order.TradeId, order.ActualAmount, time.Hour); err != nil { + t.Fatalf("lock transaction: %v", err) + } + + rawValue := big.NewInt(25_500_000) // 25.5 with 6 decimals + oldBlockTsMs := order.CreatedAt.TimestampMilli() - 1 + TryProcessEvmERC20Transfer(mdb.NetworkEthereum, contract, receiveAddress, rawValue, "0xold-hash", oldBlockTsMs) + + got, err := data.GetOrderInfoByTradeId(tradeID) + if err != nil { + t.Fatalf("load order: %v", err) + } + if got.Status != mdb.StatusWaitPay { + t.Fatalf("order status = %d, want %d", got.Status, mdb.StatusWaitPay) + } + if got.BlockTransactionId != "" { + t.Fatalf("block transaction id = %q, want empty", got.BlockTransactionId) + } + + lockTradeID, err := data.GetTradeIdByWalletAddressAndAmountAndToken(mdb.NetworkEthereum, receiveAddress.Hex(), tokenSym, amount) + if err != nil { + t.Fatalf("lookup retained lock: %v", err) + } + if lockTradeID != tradeID { + t.Fatalf("runtime lock trade_id = %q, want %q", lockTradeID, tradeID) + } +}