Learn how to fully transfer your ERC-20 token balance from one Ethereum wallet to another using MetaMask and raw JavaScript, leaving no residual dust. This tutorial walks you through the process using Chrome DevTools.
⚠️ Prerequisite: Fund the Wallet With ETH
ERC-20 token transfers require ETH to pay gas fees.
💸 How Much ETH Do You Need?
Action | Gas Estimate | ETH at 25 gwei | ETH at 40 gwei |
---|---|---|---|
ERC-20 transfer | ~50,000–60,000 gas | ~0.00125 ETH | ~0.002 ETH |
To be safe, fund the source wallet with ~0.01 ETH. If gas prices are high, consider waiting for a weekend or late-night window when network demand is lower.
🔧 Step-by-Step: Transfer Full ERC-20 Balance
✅ Step 1: Open Chrome and Unlock MetaMask
Log in to MetaMask using your source wallet (the one holding the ERC-20 balance and ETH).
✅ Step 2: Open DevTools → Console (F12 or Ctrl+Shift+I)
In the Console tab, type:
window.ethereum
If it's undefined, follow the fixes below.
🧰 Fixing MetaMask Injection Issues
If window.ethereum is not available:
✅ Check MetaMask Extension
Go to chrome://extensions/ and verify:
- MetaMask is installed
- It’s enabled
- Not in an error or paused state
✅ Step 2: Use a Real HTTPS Page
MetaMask doesn't inject into:
- file:// pages
- Some localhost pages
- Blank tabs or popups
- Use a trusted site like:
✅ Step 3: Ensure MetaMask Permissions
- Click the MetaMask icon
- If it says "Connect to site", click Connect
- Refresh the page
✅ Step 3: Paste This Script in DevTools
Once window.ethereum is available, paste the following script into the console:
(async () => {
const tokenAddress = "0xERC20ContractAddress"; // Replace with actual ERC-20 contract
const targetAddress = "0xTargetAddress"; // Destination address
const [sourceWallet] = await ethereum.request({ method: 'eth_requestAccounts' });
// 1. Fetch token balance
const paddedAddr = sourceWallet.slice(2).padStart(64, '0');
let balance = 0n;
try {
const balanceHex = await ethereum.request({
method: 'eth_call',
params: [{ to: tokenAddress, data: '0x70a08231' + paddedAddr }, 'latest']
});
balance = BigInt(balanceHex);
console.log("Raw balance:", balance.toString());
} catch (e) {
console.error("Failed to read token balance:", e.message);
return;
}
// 2. Build transfer() data
const toPadded = targetAddress.slice(2).padStart(64, '0');
const valuePadded = balance.toString(16).padStart(64, '0');
const txData = '0xa9059cbb' + toPadded + valuePadded;
// 3. Send transaction
const txHash = await ethereum.request({
method: 'eth_sendTransaction',
params: [{
from: sourceWallet,
to: tokenAddress,
data: txData
}]
});
console.log("✅ ERC-20 full transfer sent. TX Hash:", txHash);
})();
🔍 Deep Dive: Understanding eth_call
, Function Selectors, and ABI Encoding
After you paste and run the script, it may seem magical — but here's how it works under the hood.
🧠 What is eth_call
?
eth_call is an Ethereum JSON-RPC method that simulates a contract call locally without broadcasting a real transaction. It lets us read data from smart contracts (like balances or allowances) without paying gas.
🧾 Understanding data: 0x70a08231
Every function in a smart contract is identified by a 4-byte function selector, which is the first 4 bytes of the keccak256 hash of the function signature.
Example for balanceOf(address):
- Function Signature: "balanceOf(address)"
- keccak256: 0x70a082310000000000...
- Selector: 0x70a08231
- So calling balanceOf(address) with ABI-encoded arguments begins with 0x70a08231.
📚 Common ERC-20 Function Selectors
Function | Signature | Selector |
---|---|---|
Get token balance | balanceOf(address) |
0x70a08231 |
Transfer tokens | transfer(address,uint256) |
0xa9059cbb |
You can generate these with tools like: https://pi7.org/hash/keccak-256
Just paste in the raw signature string, like "balanceOf(address)"
📦 ABI Argument Encoding
Smart contract arguments must be padded to 32 bytes (64 hex characters). For example:
If calling balanceOf("0x1234...5678"), the full data field is:
0x70a08231
+ 0000000000000000000000001234567890abcdef1234567890abcdef12345678
- The first part 0x70a08231 is the function selector
- The second part is the address, left-padded with zeros to 32 bytes
To get the ERC-20 token balance:
const paddedAddr = wallet.slice(2).padStart(64, '0');
const data = '0x70a08231' + paddedAddr;
To send the full balance via transfer():
const toPadded = target.slice(2).padStart(64, '0');
const amountPadded = balance.toString(16).padStart(64, '0');
const txData = '0xa9059cbb' + toPadded + amountPadded;
Understanding this encoding lets you interact with any smart contract method manually — without relying on third-party libraries.
✅ Confirm and Sign in MetaMask
MetaMask will prompt you to approve the token transfer.
Once confirmed and mined, your ERC-20 balance will be fully transferred, with no dust left behind.
🧠 Why Not Just Use Wallet UIs?
Most wallets (like MetaMask, Rabby, etc.) limit the number of decimal places shown or used during transfers. As a result, they may leave small residual amounts (“dust”) in the wallet to avoid failed transactions due to precision or gas misestimation.
By using raw eth_call and manual ABI encoding, you can:
- Move the exact token amount
- Pay only the required gas
- Leave nothing behind
🔖 Bookmark This for Future Sweeps
Whether you're rotating wallets, archiving an old address, or just OCD about balance dust — this technique gives you full control.
🧹 Clean wallet, clean mind.
As a prerequisite, you may want to review my earlier guide: How to Fully Empty a Wallet via Chrome Console Without Leaving Dust.
Top comments (0)