Skip to content

Fix native coin forward gas buffer#3529

Merged
TaprootFreak merged 5 commits intodevelopfrom
fix/evm-coin-forward-gas-buffer
Mar 31, 2026
Merged

Fix native coin forward gas buffer#3529
TaprootFreak merged 5 commits intodevelopfrom
fix/evm-coin-forward-gas-buffer

Conversation

@TaprootFreak
Copy link
Copy Markdown
Collaborator

@TaprootFreak TaprootFreak commented Mar 30, 2026

Problem

Native coin forwards (ETH, MATIC, etc.) können in einer Endlosschleife stecken bleiben, wenn der Deposit-Betrag klein ist.

Ursache

Beim Forward von nativen Coins wird der Send-Betrag berechnet als:

sendAmount = depositAmount - estimatedGasFee × 1.00001

Die estimatedGasFee stammt aus einem 30-Sekunden-Cache (CacheItemResetPeriod.EVERY_30_SECONDS in der Payout-Strategy). Beim tatsächlichen Senden in sendNativeCoin wird aber ein frischer Gas-Preis via getRecommendedGasPrice() geholt. Zwischen dem gecachten Wert und dem frischen Wert kann sich der Gas-Preis ändern, und der bisherige Puffer von 1.00001 (0.001%) reicht nicht aus, um das abzufangen:

sendAmount + actualGasCost > walletBalance

Die Transaktion wird zwar signiert und an den RPC-Node geschickt (TX-Hash wird zurückgegeben), aber sofort aus dem Mempool gedroppt weil die Balance nicht reicht.

Auswirkung

  1. Forward wird ausgeführt → outTxId wird gespeichert, Status = Forwarded
  2. TX existiert nicht on-chain → getTxReceipt() gibt null → Confirmation schlägt fehl
  3. Nach 30 Minuten → Timeout, Reset auf Acknowledged, outTxId gelöscht
  4. Forward-Job pickt den Pay-In wieder auf → gleiche Fee-Schätzung → gleicher Fehler
  5. Endlosschleife: Forwarded → Timeout → Acknowledged → Forwarded → Timeout → ...

Zusätzlich: Falls ein Pay-In im PREPARED-Status stecken bleibt (dispatch Exception), wird beim nächsten Cron-Lauf die alte gespeicherte Fee wiederverwendet, die noch stärker veraltet sein kann.

Konkretes Beispiel (Transaction 307721)

  • ETH→ETH Swap, Deposit: 0.01 ETH
  • Gas-Fee-Schätzung (gecacht): 0.000003 ETH (≈ 0.14 gwei)
  • sendAmount = 0.01 - 0.000003 × 1.00001 = 0.009997 ETH
  • Aktueller Gas-Preis beim Senden: 0.18 gwei
  • actualGasCost = 21000 × 0.18 gwei = 0.0000039 ETH
  • sendAmount + actualGasCost = 0.010001 ETH > 0.01 ETH

Verifiziert on-chain:

  • Deposit-Wallet (0xdaee...d71ec) hat noch exakt 0.01 ETH (unverändert)
  • Nonce = 0 (keine TX wurde je von dieser Adresse gesendet)
  • Beide Forward-TX-Hashes existieren nicht auf Ethereum Mainnet

Fix

Statt die gecachte Fee-Schätzung mit einem minimalen Puffer zu verwenden, wird jetzt der frische Gas-Preis zum Zeitpunkt des Sendens geholt:

// vorher: gecachte Schätzung mit 0.001% Puffer
const amount = groupAmount - estimatedNativeFee * 1.00001;

// nachher: frischer Gas-Preis mit 50% Puffer
const freshGasCost = await this.payInEvmService.getGasCostForCoinTransaction();
const gasCost = Math.max(freshGasCost, estimatedNativeFee);
const amount = groupAmount - gasCost * 1.5;

Warum das funktioniert

  1. Frischer Gas-Preis: getGasCostForCoinTransaction() ruft gasLimit × getRecommendedGasPrice() direkt am RPC-Node ab — kein Cache
  2. Minimaler Time-Gap: Zwischen getGasCostForCoinTransaction() und sendNativeCoin() vergehen nur Millisekunden (gleicher Code-Pfad). Gas ändert sich auf Ethereum nur an Block-Grenzen (~12s)
  3. 50% Puffer auf frischem Wert: Deckt minimale Schwankungen zwischen den beiden RPC-Calls ab
  4. Fallback auf gespeicherte Schätzung: Math.max(freshGasCost, estimatedNativeFee) nimmt den höheren Wert

Für das konkrete Beispiel:

  • freshGasCost = 0.00000378 ETH (21000 × 0.18 gwei)
  • gasCost = max(0.00000378, 0.000003) = 0.00000378
  • reserved = 0.00000378 × 1.5 = 0.00000567
  • sendAmount = 0.01 - 0.00000567 = 0.00999433 ETH
  • total = 0.00999433 + 0.00000378 = 0.00999811 ETH < 0.01 ETH

Änderungen

Datei Änderung
payin-evm.service.ts Neue Methode getGasCostForCoinTransaction() die den frischen Gas-Preis vom Client holt
evm-coin.strategy.ts dispatchSend verwendet frischen Gas-Preis statt gecachter Schätzung

Test plan

  • Verify stuck ETH→ETH swap (Transaction 307721 / crypto_input 431237) resolves after deployment
  • Monitor native coin forwards on Ethereum and L2 chains for successful completion
  • Check that dust amount left in deposit wallets after forward is negligible (~0.000002 ETH)

Increase gas fee buffer from 1.00001x to 2x when calculating the send
amount for native coin forwards. The previous 0.001% buffer was
insufficient to handle gas price fluctuations between the cached fee
estimation and actual send, causing value + gas to exceed the wallet
balance. This resulted in transactions being dropped from the mempool
and an infinite forward/timeout/reset loop.
@github-actions
Copy link
Copy Markdown

github-actions bot commented Mar 30, 2026

❌ Security: 1 critical vulnerabilities

Instead of relying on a cached fee estimate (30s TTL) to calculate
the send amount, fetch the current gas cost at dispatch time. This
eliminates the race condition where gas price changes between the
cached estimation and actual send, causing value + gas > balance
and the transaction to be dropped from the mempool.
Fresh gas cost is fetched milliseconds before the actual send,
so only a minimal buffer is needed for potential block boundary
gas price changes (max 12.5% per block via EIP-1559).
The return path for native coins had no gas deduction at all,
which would cause the same value + gas > balance issue when
chargebackAmount equals the full deposit amount.
Resolves 8 security advisories including JS injection, prototype
pollution, and DoS via malformed decorator syntax.
@TaprootFreak TaprootFreak merged commit 9bc1365 into develop Mar 31, 2026
8 checks passed
@TaprootFreak TaprootFreak deleted the fix/evm-coin-forward-gas-buffer branch March 31, 2026 08:59
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