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)