From 9a6d1f62551faf9257b71592e76f19cf099e8884 Mon Sep 17 00:00:00 2001 From: sneak Date: Thu, 26 Feb 2026 15:29:48 +0700 Subject: [PATCH] Add dust transaction filter to catch native ETH poisoning Address poisoning attacks also use real native ETH dust transfers (e.g. 1 gwei) from look-alike addresses. Token-level filters cannot catch these. Add a configurable dust threshold (default 100,000 gwei / 0.0001 ETH) that hides transactions below the threshold from history. The threshold is editable in Settings and the filter can be disabled entirely. Document the specific attack tx in the README. --- README.md | 19 +++++++++++++++---- src/popup/index.html | 16 ++++++++++++++++ src/popup/views/addressDetail.js | 2 ++ src/popup/views/settings.js | 15 +++++++++++++++ src/shared/state.js | 12 ++++++++++++ src/shared/transactions.js | 14 +++++++++++++- 6 files changed, 73 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 67d4fbf..50eddd0 100644 --- a/README.md +++ b/README.md @@ -524,11 +524,22 @@ indexes it as a real token transfer. accidentally interacting with a spoofed token that appeared in their balance via a fake Transfer event. +- **Dust transaction filtering**: A second wave of the same attack used real + native ETH transfers instead of fake tokens. Transaction + `0x2708ebddfb9b5fa3f7a89d3ea398ef9fd8771b83ed861ecb7c21cd55d18edc74` sent 1 + gwei (0.000000001 ETH) from `0xC3c6B3b4402bD78A9582aB6b00E747769344F37E` — + another look-alike of the legitimate recipient `0xC3c693...`. Because this is + a real ETH transfer (not a fake token), none of the token-level filters catch + it. AutistMask hides transactions below a configurable dust threshold + (default: 100,000 gwei / 0.0001 ETH). This is high enough to filter poisoning + dust while low enough to preserve any transfer a user would plausibly care + about. The threshold is user-configurable in Settings. + - **User-configurable**: All of the above filters (known symbol verification, - low-holder threshold, fraud contract blocklist) are settings that default to - on but can be individually disabled by the user. AutistMask is designed as a - sharp tool — users who understand the risks can configure the wallet to show - everything unfiltered, unix-style. + low-holder threshold, fraud contract blocklist, dust threshold) are settings + that default to on but can be individually disabled by the user. AutistMask is + designed as a sharp tool — users who understand the risks can configure the + wallet to show everything unfiltered, unix-style. ### Non-Goals diff --git a/src/popup/index.html b/src/popup/index.html index 4211288..4dcb69a 100644 --- a/src/popup/index.html +++ b/src/popup/index.html @@ -565,6 +565,22 @@ /> Hide transactions from detected fraud contracts + +
+ + gwei +
diff --git a/src/popup/views/addressDetail.js b/src/popup/views/addressDetail.js index 571c69b..4f104ec 100644 --- a/src/popup/views/addressDetail.js +++ b/src/popup/views/addressDetail.js @@ -94,6 +94,8 @@ async function loadTransactions(address) { const result = filterTransactions(rawTxs, { hideLowHolderTokens: state.hideLowHolderTokens, hideFraudContracts: state.hideFraudContracts, + hideDustTransactions: state.hideDustTransactions, + dustThresholdGwei: state.dustThresholdGwei, fraudContracts: state.fraudContracts, }); const txs = result.transactions; diff --git a/src/popup/views/settings.js b/src/popup/views/settings.js index 5cc23df..ae44dd5 100644 --- a/src/popup/views/settings.js +++ b/src/popup/views/settings.js @@ -125,6 +125,21 @@ function init(ctx) { await saveState(); }); + $("settings-hide-dust").checked = state.hideDustTransactions; + $("settings-hide-dust").addEventListener("change", async () => { + state.hideDustTransactions = $("settings-hide-dust").checked; + await saveState(); + }); + + $("settings-dust-threshold").value = state.dustThresholdGwei; + $("settings-dust-threshold").addEventListener("change", async () => { + const val = parseInt($("settings-dust-threshold").value, 10); + if (!isNaN(val) && val >= 0) { + state.dustThresholdGwei = val; + await saveState(); + } + }); + $("btn-main-add-wallet").addEventListener("click", ctx.showAddWalletView); $("btn-settings-back").addEventListener("click", () => { diff --git a/src/shared/state.js b/src/shared/state.js index eb7e65d..5bac05c 100644 --- a/src/shared/state.js +++ b/src/shared/state.js @@ -20,6 +20,8 @@ const DEFAULT_STATE = { rememberSiteChoice: true, hideLowHolderTokens: true, hideFraudContracts: true, + hideDustTransactions: true, + dustThresholdGwei: 100000, fraudContracts: [], tokenHolderCache: {}, }; @@ -44,6 +46,8 @@ async function saveState() { rememberSiteChoice: state.rememberSiteChoice, hideLowHolderTokens: state.hideLowHolderTokens, hideFraudContracts: state.hideFraudContracts, + hideDustTransactions: state.hideDustTransactions, + dustThresholdGwei: state.dustThresholdGwei, fraudContracts: state.fraudContracts, tokenHolderCache: state.tokenHolderCache, }; @@ -82,6 +86,14 @@ async function loadState() { saved.hideFraudContracts !== undefined ? saved.hideFraudContracts : true; + state.hideDustTransactions = + saved.hideDustTransactions !== undefined + ? saved.hideDustTransactions + : true; + state.dustThresholdGwei = + saved.dustThresholdGwei !== undefined + ? saved.dustThresholdGwei + : 100000; state.fraudContracts = saved.fraudContracts || []; state.tokenHolderCache = saved.tokenHolderCache || {}; } diff --git a/src/shared/transactions.js b/src/shared/transactions.js index 9cca8dc..b3d6b62 100644 --- a/src/shared/transactions.js +++ b/src/shared/transactions.js @@ -20,13 +20,15 @@ function formatTxValue(val) { function parseTx(tx, addrLower) { const from = tx.from?.hash || ""; const to = tx.to?.hash || ""; + const rawWei = tx.value || "0"; return { hash: tx.hash, blockNumber: tx.block_number, timestamp: Math.floor(new Date(tx.timestamp).getTime() / 1000), from: from, to: to, - value: formatTxValue(formatEther(tx.value || "0")), + value: formatTxValue(formatEther(rawWei)), + valueGwei: Math.floor(Number(BigInt(rawWei) / BigInt(1000000000))), symbol: "ETH", direction: from.toLowerCase() === addrLower ? "sent" : "received", isError: tx.status !== "ok", @@ -47,6 +49,7 @@ function parseTokenTransfer(tt, addrLower) { from: from, to: to, value: formatTxValue(formatUnits(rawValue, decimals)), + valueGwei: null, symbol: tt.token?.symbol || "?", direction: from.toLowerCase() === addrLower ? "sent" : "received", isError: false, @@ -160,6 +163,15 @@ function filterTransactions(txs, filters = {}) { continue; } + // Filter dust transactions (below gwei threshold) if setting is on + if ( + filters.hideDustTransactions && + tx.valueGwei !== null && + tx.valueGwei < (filters.dustThresholdGwei || 100000) + ) { + continue; + } + filtered.push(tx); }