Token-0x

First Flight #54
Beginner FriendlyDeFi
100 EXP
Submission Details
Impact: medium
Likelihood: high

Non-Compliant `balanceOf` and `allowance` Revert on Zero Address, Breaking ERC-20 Standard and Integrations

Author Revealed upon completion

Root + Impact

Description

  • The ERC-20 standard specifies balanceOf(address) and allowance(address,address) as view functions that return uint256 values without mandating reverts for address(0) inputs. Standard-compliant implementations (OpenZeppelin ERC20) return 0 for uninitialized mapping slots at address(0).

  • In ERC20Internals.sol, internal view functions explicitly revert:

// _balanceOf
if iszero(owner) {
revert(0, 0)
}
// _allowance
if or(iszero(owner), iszero(spender)) {
revert(0, 0)
}

Risk

Likelihood:

  • 100% for calls with zero address (every such call will revert).


Impact:

  • Integration DoS: Contracts and off-chain processes that expect a 0 result will get a revert, potentially breaking transaction flows.

  • Standard Violation: This deviates from commonly accepted ERC-20 behavior and OpenZeppelin reference implementation.

Proof of Concept

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;
import {Test, console} from "forge-std/Test.sol";
import {ERC20} from "../src/ERC20.sol";
contract ERC20Mock is ERC20 {
constructor() ERC20("Test", "TST") {}
}
contract ComplianceTest is Test {
ERC20Mock public token;
function setUp() public {
token = new ERC20Mock();
}
function test_balanceOf_zeroAddress_reverts() public {
// Standard ERC20 should return 0. This contract reverts.
// Expect Revert
vm.expectRevert();
token.balanceOf(address(0));
}
function test_allowance_zeroAddress_reverts() public {
// Standard ERC20 should return 0. This contract reverts.
// Expect Revert
vm.expectRevert();
token.allowance(address(0), address(1));
}
}

Run:

forge test --match-contract ComplianceTest -vvvv

Output:

[PASS] test_allowance_zeroAddress_reverts() (gas: 9331)
Traces:
[9331] ComplianceTest::test_allowance_zeroAddress_reverts()
├─ [0] VM::expectRevert(custom error 0xf4844814)
│ └─ ← [Return]
├─ [802] ERC20Mock::allowance(0x0000000000000000000000000000000000000000, ECRecover: [0x0000000000000000000000000000000000000001]) [staticcall]
│ └─ ← [Revert] EvmError: Revert
└─ ← [Stop]
[PASS] test_balanceOf_zeroAddress_reverts() (gas: 8904)
Traces:
[8904] ComplianceTest::test_balanceOf_zeroAddress_reverts()
├─ [0] VM::expectRevert(custom error 0xf4844814)
│ └─ ← [Return]
├─ [502] ERC20Mock::balanceOf(0x0000000000000000000000000000000000000000) [staticcall]
│ └─ ← [Revert] EvmError: Revert
└─ ← [Stop]

Recommended Mitigation

function _balanceOf(address owner) internal view returns (uint256) {
assembly {
- if iszero(owner) {
- revert(0, 0)
- }
let baseSlot := _balances.slot
// ... rest of the code
}
}
function _allowance(address owner, address spender) internal view returns (uint256 remaining) {
assembly {
- if or(iszero(owner), iszero(spender)) {
- revert(0, 0)
- }
let ptr := mload(0x40)
// ... rest of the code
}
}

Support

FAQs

Can't find an answer? Chat with us on Discord, Twitter or Linkedin.

Give us feedback!