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); }