Detecting the silent scam that tricks users into sending funds to the wrong address — without storing any data.
The Silent Killer of Crypto
Address Poisoning is one of the most psychologically deceptive scams in Web3. It doesn't exploit smart contracts. It doesn't use malicious code. It exploits human habit.
Here's how it works:
- You send 100 USDC to your friend at
0x1234...5678 - A scammer bot monitors the mempool and sees this transaction
- The scammer sends a 0-value transaction FROM an address that looks almost identical:
0x1234...5679 - Both transactions now appear in your MetaMask activity history
- Next time you want to send funds to your friend, you copy the address from your history
- You accidentally copy the scammer's address (because it looks 99% identical)
- You send your funds to the scammer
The scary part? No security tool catches this. The transaction is technically valid. The address is real. The wallet sends it. The funds are gone.
Why Traditional Tools Miss It
Most crypto security extensions rely on blacklists — databases of known scam addresses. But address poisoning doesn't use "known scam addresses." It uses freshly generated wallets that:
- Have no transaction history
- Aren't on any blacklist
- Look identical to legitimate addresses
- Change with every attack
A blacklist-based tool sees the transaction and says: "This address isn't on our bad list. Looks safe!"
But it's not safe. It's a trap.
The Solution: Local History + Heuristic Matching
I realized the answer wasn't a better database — it was a better pattern.
Instead of checking if an address is "known bad," we check if it looks suspiciously similar to one the user has interacted with before.
Here's the architecture:
- Track every address the user successfully interacts with (stored locally)
- Compare new transaction targets against this local history
- Detect if a new address shares the first 6 and last 4 characters with a known address
- Block the transaction with a visual warning
No database. No API calls. No tracking. 100% local.
The Technical Implementation
Step 1: The History Bridge
Because the core interception engine runs in the browser's MAIN world (to hook into window.ethereum), it can't access Chrome's storage API directly. So I created a bridge — a content script that listens for messages from the engine and handles storage.
// contents/history-bridge.ts
window.addEventListener("message", async (event) => {
// Check if an address is potentially poisoned
if (event.data.type === "SAFESIGN_CHECK_POISON") {
const checkAddress = event.data.payload.toLowerCase()
// Fetch previously interacted addresses from local storage
const result = await chrome.storage.local.get("safesign_history")
const history: string[] = result.safesign_history || []
let isPoison = false
let matchedSafeAddress = ""
for (const safeAddr of history) {
if (safeAddr === checkAddress) {
// Exact match = definitely safe
isPoison = false
break
}
// Heuristic: Check first 6 and last 4 characters
if (safeAddr.length >= 10 && checkAddress.length >= 10) {
const safeStart = safeAddr.slice(0, 8) // "0x" + 6 chars
const safeEnd = safeAddr.slice(-4)
const checkStart = checkAddress.slice(0, 8)
const checkEnd = checkAddress.slice(-4)
if (safeStart === checkStart && safeEnd === checkEnd) {
// Similar but NOT exact = poison!
isPoison = true
matchedSafeAddress = safeAddr
break
}
}
}
// Send result back to the engine
window.postMessage({
type: "SAFESIGN_POISON_RESULT",
payload: { isPoison, matchedSafeAddress, checkAddress }
}, "*")
}
// Add safe addresses to history after successful transactions
if (event.data.type === "SAFESIGN_ADD_HISTORY") {
const newAddress = event.data.payload.toLowerCase()
const result = await chrome.storage.local.get("safesign_history")
const history: string[] = result.safesign_history || []
if (!history.includes(newAddress)) {
history.push(newAddress)
// Cap at 100 addresses to save space
if (history.length > 100) history.shift()
await chrome.storage.local.set({ safesign_history: history })
}
}
})
Step 2: The Detection Engine
In the main interception engine, before any transaction passes through to wallet, we ask the bridge to check the destination address:
// contents/ethereum-listener.ts
if (args.method === "eth_sendTransaction") {
const toAddress = txParams?.to?.toLowerCase()
const fromAddress = txParams?.from?.toLowerCase()
// Address Poisoning Check
if (toAddress && fromAddress) {
const poisonCheck = await checkAddressPoisoning(toAddress)
if (poisonCheck.isPoison) {
// BLOCK: Show the Red Warning UI
window.postMessage({
type: "SAFESIGN_ALERT",
payload: {
dangerType: "POISONED_ADDRESS",
scamAddress: toAddress,
rawData: `Similar to: ${poisonCheck.matchedSafeAddress}`
}
}, "*")
throw new Error("SafeSign: Blocked Address Poisoning")
}
}
// ... other security checks (blacklist, approvals, permits, etc.)
// If all checks pass, add to history
if (toAddress) {
window.postMessage({
type: "SAFESIGN_ADD_HISTORY",
payload: toAddress
}, "*")
}
}
The checkAddressPoisoning helper uses a Promise-based message pattern to communicate with the bridge:
const checkAddressPoisoning = (address: string) => {
return new Promise<{ isPoison: boolean, matchedSafeAddress: string }>((resolve) => {
const handleMessage = (event: MessageEvent) => {
if (event.data.type === "SAFESIGN_POISON_RESULT") {
window.removeEventListener("message", handleMessage)
resolve(event.data.payload)
}
}
window.addEventListener("message", handleMessage)
window.postMessage({ type: "SAFESIGN_CHECK_POISON", payload: address }, "*")
})
}
The Heuristic: Why 6 and 4?
You might wonder: why check the first 6 and last 4 characters?
Ethereum addresses look like this: 0x + 40 hex characters.
Scammers typically create vanity addresses that match:
- First 4-6 characters (what users visually scan)
- Last 4 characters (what users visually scan)
By checking 6 + 4, we catch:
- ✅ The most common poisoning pattern
- ✅ False positives are rare (1 in 4 billion chance of accidental match)
- ✅ It's fast (simple string comparison)
Could we check more characters? Yes, but:
- More characters = more false negatives (scammers might only match 4+4)
- The 6+4 heuristic is the sweet spot between safety and accuracy
The Privacy Architecture
This was the most important design decision: zero data leaves the browser.
- ✅ No backend server
- ✅ No API calls
- ✅ No analytics
- ✅ No tracking
- ✅ History capped at 100 addresses (~4.2 KB)
- ✅ Stored in
chrome.storage.local(user's hard drive, not cloud)
The UX Decision: Why a Red Warning?
When the detector triggers, SafeSign doesn't just silently block the transaction. It shows the Time-Travel UI with a specific message:
"ADDRESS POISONING DETECTED"
This address looks almost identical to one you have used before, but it is a fake. Sending funds here will lose them forever.
Why not a passive notification? Because:
- Users need to understand WHY — otherwise they'll just retry with the same poisoned address
- Visual storytelling beats technical jargon — "looks almost identical" is something anyone understands
- The Red UI creates a moment of pause — users stop and think before acting
What This Catches (And What It Doesn't)
✅ Catches:
- Classic address poisoning (similar start + end)
- Vanity address spoofing
- Zero-value token transfers that pollute transaction history
❌ Doesn't Catch (Yet):
- Addresses that only match 4+3 characters (too aggressive to block)
- Addresses with no prior history to compare against
- Cross-chain poisoning (Solana, etc.) — coming in v2.0
The Bigger Picture
Address Poisoning is just one of the scams SafeSign blocks. But it represents something bigger: the philosophy of proactive, local-first security.
Instead of:
- ❌ Waiting for someone to report a scam address
- ❌ Maintaining a massive database
- ❌ Sending user data to a server for analysis
We:
- ✅ Analyze patterns locally
- ✅ Protect privacy by design
- ✅ Catch zero-day scams that no database knows about
This is the future of crypto security — not bigger databases, but smarter heuristics.
Try It
SafeSign is 100% free and open-source. If you want to protect yourself from address poisoning (and 6 other scam types), install it today.
🔗 Chrome Web Store: SafeSign Visualizer
💻 GitHub: XenoVenom/safesign-visualizer
🌐 Landing Page: https://safesignvisualizer.vercel.app/
This is Devlog #2 in a series about building SafeSign. In the next devlog, I'll break down how I intercept gasless EIP-2612 Permit signatures before wallet opens.

Top comments (0)