Add dust transaction filter to catch native ETH poisoning
Some checks failed
check / check (push) Has been cancelled

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.
This commit is contained in:
2026-02-26 15:29:48 +07:00
parent b5b4f75968
commit 9a6d1f6255
6 changed files with 73 additions and 5 deletions

View File

@@ -524,11 +524,22 @@ indexes it as a real token transfer.
accidentally interacting with a spoofed token that appeared in their balance accidentally interacting with a spoofed token that appeared in their balance
via a fake Transfer event. 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, - **User-configurable**: All of the above filters (known symbol verification,
low-holder threshold, fraud contract blocklist) are settings that default to low-holder threshold, fraud contract blocklist, dust threshold) are settings
on but can be individually disabled by the user. AutistMask is designed as a that default to on but can be individually disabled by the user. AutistMask is
sharp tool — users who understand the risks can configure the wallet to show designed as a sharp tool — users who understand the risks can configure the
everything unfiltered, unix-style. wallet to show everything unfiltered, unix-style.
### Non-Goals ### Non-Goals

View File

@@ -565,6 +565,22 @@
/> />
Hide transactions from detected fraud contracts Hide transactions from detected fraud contracts
</label> </label>
<label
class="text-xs flex items-center gap-1 cursor-pointer mt-1"
>
<input type="checkbox" id="settings-hide-dust" />
Hide dust transactions below
</label>
<div class="flex items-center gap-1 mt-1">
<input
type="number"
id="settings-dust-threshold"
class="border border-border p-1 text-xs bg-bg text-fg"
style="width: 10ch"
min="0"
/>
<span class="text-xs text-muted">gwei</span>
</div>
</div> </div>
<div class="bg-well p-3 mx-1 mb-3"> <div class="bg-well p-3 mx-1 mb-3">

View File

@@ -94,6 +94,8 @@ async function loadTransactions(address) {
const result = filterTransactions(rawTxs, { const result = filterTransactions(rawTxs, {
hideLowHolderTokens: state.hideLowHolderTokens, hideLowHolderTokens: state.hideLowHolderTokens,
hideFraudContracts: state.hideFraudContracts, hideFraudContracts: state.hideFraudContracts,
hideDustTransactions: state.hideDustTransactions,
dustThresholdGwei: state.dustThresholdGwei,
fraudContracts: state.fraudContracts, fraudContracts: state.fraudContracts,
}); });
const txs = result.transactions; const txs = result.transactions;

View File

@@ -125,6 +125,21 @@ function init(ctx) {
await saveState(); 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-main-add-wallet").addEventListener("click", ctx.showAddWalletView);
$("btn-settings-back").addEventListener("click", () => { $("btn-settings-back").addEventListener("click", () => {

View File

@@ -20,6 +20,8 @@ const DEFAULT_STATE = {
rememberSiteChoice: true, rememberSiteChoice: true,
hideLowHolderTokens: true, hideLowHolderTokens: true,
hideFraudContracts: true, hideFraudContracts: true,
hideDustTransactions: true,
dustThresholdGwei: 100000,
fraudContracts: [], fraudContracts: [],
tokenHolderCache: {}, tokenHolderCache: {},
}; };
@@ -44,6 +46,8 @@ async function saveState() {
rememberSiteChoice: state.rememberSiteChoice, rememberSiteChoice: state.rememberSiteChoice,
hideLowHolderTokens: state.hideLowHolderTokens, hideLowHolderTokens: state.hideLowHolderTokens,
hideFraudContracts: state.hideFraudContracts, hideFraudContracts: state.hideFraudContracts,
hideDustTransactions: state.hideDustTransactions,
dustThresholdGwei: state.dustThresholdGwei,
fraudContracts: state.fraudContracts, fraudContracts: state.fraudContracts,
tokenHolderCache: state.tokenHolderCache, tokenHolderCache: state.tokenHolderCache,
}; };
@@ -82,6 +86,14 @@ async function loadState() {
saved.hideFraudContracts !== undefined saved.hideFraudContracts !== undefined
? saved.hideFraudContracts ? saved.hideFraudContracts
: true; : true;
state.hideDustTransactions =
saved.hideDustTransactions !== undefined
? saved.hideDustTransactions
: true;
state.dustThresholdGwei =
saved.dustThresholdGwei !== undefined
? saved.dustThresholdGwei
: 100000;
state.fraudContracts = saved.fraudContracts || []; state.fraudContracts = saved.fraudContracts || [];
state.tokenHolderCache = saved.tokenHolderCache || {}; state.tokenHolderCache = saved.tokenHolderCache || {};
} }

View File

@@ -20,13 +20,15 @@ function formatTxValue(val) {
function parseTx(tx, addrLower) { function parseTx(tx, addrLower) {
const from = tx.from?.hash || ""; const from = tx.from?.hash || "";
const to = tx.to?.hash || ""; const to = tx.to?.hash || "";
const rawWei = tx.value || "0";
return { return {
hash: tx.hash, hash: tx.hash,
blockNumber: tx.block_number, blockNumber: tx.block_number,
timestamp: Math.floor(new Date(tx.timestamp).getTime() / 1000), timestamp: Math.floor(new Date(tx.timestamp).getTime() / 1000),
from: from, from: from,
to: to, to: to,
value: formatTxValue(formatEther(tx.value || "0")), value: formatTxValue(formatEther(rawWei)),
valueGwei: Math.floor(Number(BigInt(rawWei) / BigInt(1000000000))),
symbol: "ETH", symbol: "ETH",
direction: from.toLowerCase() === addrLower ? "sent" : "received", direction: from.toLowerCase() === addrLower ? "sent" : "received",
isError: tx.status !== "ok", isError: tx.status !== "ok",
@@ -47,6 +49,7 @@ function parseTokenTransfer(tt, addrLower) {
from: from, from: from,
to: to, to: to,
value: formatTxValue(formatUnits(rawValue, decimals)), value: formatTxValue(formatUnits(rawValue, decimals)),
valueGwei: null,
symbol: tt.token?.symbol || "?", symbol: tt.token?.symbol || "?",
direction: from.toLowerCase() === addrLower ? "sent" : "received", direction: from.toLowerCase() === addrLower ? "sent" : "received",
isError: false, isError: false,
@@ -160,6 +163,15 @@ function filterTransactions(txs, filters = {}) {
continue; 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); filtered.push(tx);
} }