From 96d47ba8024771d6bc39157f782c88b9a74a01e4 Mon Sep 17 00:00:00 2001 From: TaprootFreak <142087526+TaprootFreak@users.noreply.github.com> Date: Mon, 30 Mar 2026 19:09:21 +0200 Subject: [PATCH 1/5] Fix native coin forward gas buffer to prevent stuck transactions 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. --- .../payin/strategies/send/impl/base/evm-coin.strategy.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/subdomains/supporting/payin/strategies/send/impl/base/evm-coin.strategy.ts b/src/subdomains/supporting/payin/strategies/send/impl/base/evm-coin.strategy.ts index 30b2766b0c..48db14d6ab 100644 --- a/src/subdomains/supporting/payin/strategies/send/impl/base/evm-coin.strategy.ts +++ b/src/subdomains/supporting/payin/strategies/send/impl/base/evm-coin.strategy.ts @@ -45,8 +45,8 @@ export abstract class EvmCoinStrategy extends EvmStrategy { const { account, destinationAddress } = payInGroup; const groupAmount = this.getTotalGroupAmount(payInGroup, type); - // subtract fee for forwarding - const amount = type === SendType.FORWARD ? Util.round(groupAmount - estimatedNativeFee * 1.00001, 12) : groupAmount; + // subtract fee for forwarding (2x buffer to handle gas price fluctuations between estimation and send) + const amount = type === SendType.FORWARD ? Util.round(groupAmount - estimatedNativeFee * 2, 12) : groupAmount; return this.payInEvmService.sendNativeCoin(account, destinationAddress, amount); } From ba60a5ba6d1ddcdcdbe55e99dee4ce5efecd8bc1 Mon Sep 17 00:00:00 2001 From: TaprootFreak <142087526+TaprootFreak@users.noreply.github.com> Date: Mon, 30 Mar 2026 19:14:53 +0200 Subject: [PATCH 2/5] Use fresh gas cost for native coin forward amount calculation 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. --- .../payin/services/base/payin-evm.service.ts | 4 ++++ .../send/impl/base/evm-coin.strategy.ts | 15 +++++++++++---- 2 files changed, 15 insertions(+), 4 deletions(-) diff --git a/src/subdomains/supporting/payin/services/base/payin-evm.service.ts b/src/subdomains/supporting/payin/services/base/payin-evm.service.ts index cc140a553d..43da7769c0 100644 --- a/src/subdomains/supporting/payin/services/base/payin-evm.service.ts +++ b/src/subdomains/supporting/payin/services/base/payin-evm.service.ts @@ -23,6 +23,10 @@ export abstract class PayInEvmService { return this.#client.sendTokenFromAccount(account, addressTo, tokenName, amount); } + async getGasCostForCoinTransaction(): Promise { + return this.#client.getCurrentGasCostForCoinTransaction(); + } + async checkTransactionCompletion(txHash: string, minConfirmations: number): Promise { return this.#client.isTxComplete(txHash, minConfirmations); } diff --git a/src/subdomains/supporting/payin/strategies/send/impl/base/evm-coin.strategy.ts b/src/subdomains/supporting/payin/strategies/send/impl/base/evm-coin.strategy.ts index 48db14d6ab..768fb129a9 100644 --- a/src/subdomains/supporting/payin/strategies/send/impl/base/evm-coin.strategy.ts +++ b/src/subdomains/supporting/payin/strategies/send/impl/base/evm-coin.strategy.ts @@ -41,13 +41,20 @@ export abstract class EvmCoinStrategy extends EvmStrategy { payInGroup.status = PayInStatus.PREPARED; } - protected dispatchSend(payInGroup: SendGroup, type: SendType, estimatedNativeFee: number): Promise { + protected async dispatchSend(payInGroup: SendGroup, type: SendType, estimatedNativeFee: number): Promise { const { account, destinationAddress } = payInGroup; const groupAmount = this.getTotalGroupAmount(payInGroup, type); - // subtract fee for forwarding (2x buffer to handle gas price fluctuations between estimation and send) - const amount = type === SendType.FORWARD ? Util.round(groupAmount - estimatedNativeFee * 2, 12) : groupAmount; - return this.payInEvmService.sendNativeCoin(account, destinationAddress, amount); + if (type === SendType.FORWARD) { + // use fresh gas cost (not cached estimate) to avoid value + gas > balance + const freshGasCost = await this.payInEvmService.getGasCostForCoinTransaction(); + const gasCost = Math.max(freshGasCost, estimatedNativeFee); + const amount = Util.round(groupAmount - gasCost * 1.5, 12); + + return this.payInEvmService.sendNativeCoin(account, destinationAddress, amount); + } + + return this.payInEvmService.sendNativeCoin(account, destinationAddress, groupAmount); } } From 7789b34ebf751d7dbc2fd0b1d235f33dd0ea8020 Mon Sep 17 00:00:00 2001 From: TaprootFreak <142087526+TaprootFreak@users.noreply.github.com> Date: Mon, 30 Mar 2026 19:16:40 +0200 Subject: [PATCH 3/5] Reduce gas buffer from 1.5x to 1.05x 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). --- .../payin/strategies/send/impl/base/evm-coin.strategy.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/subdomains/supporting/payin/strategies/send/impl/base/evm-coin.strategy.ts b/src/subdomains/supporting/payin/strategies/send/impl/base/evm-coin.strategy.ts index 768fb129a9..cbc7a3acce 100644 --- a/src/subdomains/supporting/payin/strategies/send/impl/base/evm-coin.strategy.ts +++ b/src/subdomains/supporting/payin/strategies/send/impl/base/evm-coin.strategy.ts @@ -50,7 +50,7 @@ export abstract class EvmCoinStrategy extends EvmStrategy { // use fresh gas cost (not cached estimate) to avoid value + gas > balance const freshGasCost = await this.payInEvmService.getGasCostForCoinTransaction(); const gasCost = Math.max(freshGasCost, estimatedNativeFee); - const amount = Util.round(groupAmount - gasCost * 1.5, 12); + const amount = Util.round(groupAmount - gasCost * 1.05, 12); return this.payInEvmService.sendNativeCoin(account, destinationAddress, amount); } From 8311026e7e2e0911b84a6897355b56f8b8eeb37b Mon Sep 17 00:00:00 2001 From: TaprootFreak <142087526+TaprootFreak@users.noreply.github.com> Date: Mon, 30 Mar 2026 19:20:16 +0200 Subject: [PATCH 4/5] Apply fresh gas cost deduction to both forward and return paths 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. --- .../send/impl/base/evm-coin.strategy.ts | 15 +++++---------- 1 file changed, 5 insertions(+), 10 deletions(-) diff --git a/src/subdomains/supporting/payin/strategies/send/impl/base/evm-coin.strategy.ts b/src/subdomains/supporting/payin/strategies/send/impl/base/evm-coin.strategy.ts index cbc7a3acce..9674b0b453 100644 --- a/src/subdomains/supporting/payin/strategies/send/impl/base/evm-coin.strategy.ts +++ b/src/subdomains/supporting/payin/strategies/send/impl/base/evm-coin.strategy.ts @@ -45,16 +45,11 @@ export abstract class EvmCoinStrategy extends EvmStrategy { const { account, destinationAddress } = payInGroup; const groupAmount = this.getTotalGroupAmount(payInGroup, type); + // use fresh gas cost (not cached estimate) to avoid value + gas > balance + const freshGasCost = await this.payInEvmService.getGasCostForCoinTransaction(); + const gasCost = Math.max(freshGasCost, estimatedNativeFee); + const amount = Util.round(groupAmount - gasCost * 1.05, 12); - if (type === SendType.FORWARD) { - // use fresh gas cost (not cached estimate) to avoid value + gas > balance - const freshGasCost = await this.payInEvmService.getGasCostForCoinTransaction(); - const gasCost = Math.max(freshGasCost, estimatedNativeFee); - const amount = Util.round(groupAmount - gasCost * 1.05, 12); - - return this.payInEvmService.sendNativeCoin(account, destinationAddress, amount); - } - - return this.payInEvmService.sendNativeCoin(account, destinationAddress, groupAmount); + return this.payInEvmService.sendNativeCoin(account, destinationAddress, amount); } } From 74e0915339a34cc179f7edd2c777c3c8775e5114 Mon Sep 17 00:00:00 2001 From: TaprootFreak <142087526+TaprootFreak@users.noreply.github.com> Date: Mon, 30 Mar 2026 19:29:50 +0200 Subject: [PATCH 5/5] =?UTF-8?q?Update=20handlebars=204.7.8=20=E2=86=92=204?= =?UTF-8?q?.7.9=20to=20fix=20critical=20vulnerability?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Resolves 8 security advisories including JS injection, prototype pollution, and DoS via malformed decorator syntax. --- package-lock.json | 8 ++++---- package.json | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package-lock.json b/package-lock.json index 2602a55b3f..01d7a0752b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -81,7 +81,7 @@ "geoip-lite2": "^2.2.7", "graphql": "^16.11.0", "graphql-request": "^6.1.0", - "handlebars": "^4.7.8", + "handlebars": "^4.7.9", "helmet": "^6.2.0", "ibantools": "^4.5.1", "jszip": "^3.10.1", @@ -17493,9 +17493,9 @@ } }, "node_modules/handlebars": { - "version": "4.7.8", - "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.7.8.tgz", - "integrity": "sha512-vafaFqs8MZkRrSX7sFVUdo3ap/eNiLnb4IakshzvP56X5Nr1iGKAIqdX6tMlm6HcNRIkr6AxO5jFEoJzzpT8aQ==", + "version": "4.7.9", + "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.7.9.tgz", + "integrity": "sha512-4E71E0rpOaQuJR2A3xDZ+GM1HyWYv1clR58tC8emQNeQe3RH7MAzSbat+V0wG78LQBo6m6bzSG/L4pBuCsgnUQ==", "license": "MIT", "dependencies": { "minimist": "^1.2.5", diff --git a/package.json b/package.json index 2c1ea32ccf..f02316dcf9 100644 --- a/package.json +++ b/package.json @@ -98,7 +98,7 @@ "geoip-lite2": "^2.2.7", "graphql": "^16.11.0", "graphql-request": "^6.1.0", - "handlebars": "^4.7.8", + "handlebars": "^4.7.9", "helmet": "^6.2.0", "ibantools": "^4.5.1", "jszip": "^3.10.1",