EVM / SolidityRunbook 0/4 done
0x08c379a0

EVM / Solidity Error 0x08c379a0: Error(string) revert

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

0x08c379a0 is the selector of Error(string), emitted by require(cond, "msg") and revert("msg"). ABI-decode the rest of the data to read the reason string; fix the failing condition.

Identify: does your log match?

What you see · exact log / response
ContractFunctionExecutionError: The contract function "transfer" reverted with the following reason:
ERC20: transfer amount exceeds balance

Contract Call:
  address:   0x5FbDB2315678afecb367f032d93F642f64180aa3
  function:  transfer(address to, uint256 amount)
  args:              (0x70997970C51812dc3A010C7d01b50e0d17dc79C8, 1000000000000000000000)
  sender:    0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266

Raw revert data:
0x08c379a00000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000002645524332303a207472616e7366657220616d6f756e7420657863656564732062616c616e63650000000000000000000000000000000000000000000000000000

Version: viem@2.21.37
  1. High

    require() condition failed

    A require(cond, "reason") evaluated false — balance, allowance, deadline or ownership check. The reason string is ABI-encoded after the selector.

  2. Medium

    Pre-v5 OpenZeppelin string errors

    OpenZeppelin 4.x contracts revert with strings like "ERC20: transfer amount exceeds balance"; v5 switched to custom errors.

  3. Low

    Wrong msg.sender or value in simulation

    eth_call without from, or with value 0, hits a guard that would pass for the real sender.

Fix

Replace require strings with custom errors

✕ Broken · Solidity
function withdraw(uint256 amount) external {
    require(balances[msg.sender] >= amount, "Insufficient balance");
    balances[msg.sender] -= amount;
    payable(msg.sender).transfer(amount);
}
✓ Fixed · Solidity
error InsufficientBalance(uint256 available, uint256 required);

function withdraw(uint256 amount) external {
    uint256 bal = balances[msg.sender];
    if (bal < amount) revert InsufficientBalance(bal, amount);
    balances[msg.sender] = bal - amount;
    (bool ok, ) = payable(msg.sender).call{value: amount}("");
    require(ok);
}

Custom errors are cheaper to deploy and revert, and carry typed values. call{value:} avoids the 2300-gas stipend of transfer.

Verify: reproduce it on purpose

Reproduce locally · cast
$ cast decode-error 0x08c379a00000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000002645524332303a207472616e7366657220616d6f756e7420657863656564732062616c616e63650000000000000000000000000000000000000000000000000000 --sig "Error(string)"

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

Prevent

  • Always simulate with the real from address.
  • Prefer custom errors in new contracts.
  • Decode raw data with cast decode-error.

FAQ

How do I decode 0x08c379a0 revert data by hand?

Strip the 4-byte selector and ABI-decode the remainder as (string). cast decode-error <data> --sig "Error(string)" does it in one step.

Why does ethers v6 show "execution reverted" with no reason?

The RPC did not return revert data (some providers strip it on estimateGas). Retry with eth_call or a provider that returns data.

What is the gas cost difference vs custom errors?

Custom errors save deployment size and a few hundred gas per revert because the string is not stored or ABI-encoded.

Related errors

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