EVM / SolidityRunbook 0/4 done
0xe450d38c

EVM / Solidity Error 0xe450d38c: ERC20InsufficientBalance

Error✕ Not retryable · fix the payloadSmart Contract
TL;DR · 10-second read

An OpenZeppelin v5 ERC-20 transfer or burn needed more tokens than the sender holds. The data decodes to (sender, balance, needed); top up the balance or lower the amount, minding decimals.

Identify: does your log match?

What you see · exact log / response
Error: execution reverted (unknown custom error) (action="estimateGas", data="0xe450d38c00000000000000000000000070997970c51812dc3a010c7d01b50e0d17dc79c800000000000000000000000000000000000000000000000d8d726b7177a8000000000000000000000000000000000000000000000000003635c9adc5dea00000", reason=null, transaction={ "data": "0xa9059cbb...", "from": "0x70997970C51812dc3A010C7d01b50e0d17dc79C8", "to": "0x5FbDB2315678afecb367f032d93F642f64180aa3" }, invocation=null, revert=null, code=CALL_EXCEPTION, version=6.13.4)
  1. High

    Amount not scaled by decimals

    Passing 1000 * 10**18 to a 6-decimal token (USDC) asks for 10^12 times too much. Always read decimals().

  2. Medium

    Wrong sender in transferFrom

    transferFrom checks the from address's balance, not the caller's. The vault or user you pull from is empty.

  3. Low

    Fee-on-transfer or rebasing token

    The received amount was less than expected, so a follow-up transfer of the full amount exceeds the actual balance.

Fix

Decode the custom error with the ABI (ethers v6)

✕ Broken · TypeScript
await token.transfer(to, amount); // "unknown custom error"
✓ Fixed · TypeScript
import { Contract, Interface, parseUnits } from 'ethers';

const iface = new Interface([
  'error ERC20InsufficientBalance(address sender, uint256 balance, uint256 needed)',
]);
const decimals = await token.decimals();
const amount = parseUnits('250', decimals);
try {
  await token.transfer.staticCall(to, amount);
} catch (e: any) {
  const parsed = iface.parseError(e.data);
  if (parsed?.name === 'ERC20InsufficientBalance') console.log(parsed.args.balance, parsed.args.needed);
}

Including the error in the ABI turns "unknown custom error" into named args. parseUnits with on-chain decimals avoids scale bugs.

Verify: reproduce it on purpose

Reproduce locally · cast
$ cast decode-error 0xe450d38c00000000000000000000000070997970c51812dc3a010c7d01b50e0d17dc79c800000000000000000000000000000000000000000000000d8d726b7177a8000000000000000000000000000000000000000000000000003635c9adc5dea00000 --sig "ERC20InsufficientBalance(address,uint256,uint256)"

Run it against a dev environment to trigger 0xe450d38c on purpose and test your error handler.

Prevent

  • Add OZ IERC20Errors to your frontend ABI.
  • Never hardcode 18 decimals.
  • Show balance vs needed in the UI before signing.

FAQ

How do I decode 0xe450d38c with cast?

cast decode-error <data> --sig "ERC20InsufficientBalance(address,uint256,uint256)" prints sender, balance and needed.

Which OpenZeppelin version uses this error?

OpenZeppelin Contracts v5.0+ (IERC20Errors, ERC-6093). v4 reverted with "ERC20: transfer amount exceeds balance".

Is this the same as insufficient allowance?

No. Allowance failures use ERC20InsufficientAllowance (0xfb8f41b2) and are fixed with approve().

Related errors

Last verified 2026-10-22 · Source: 4byte/OpenChain signatures + OpenZeppelin v5 custom errors + Panic codes · Official docs ↗ · ~3,600 searches/mo