Token-0x

First Flight #54
Beginner FriendlyDeFi
100 EXP
View results
Submission Details
Severity: low
Valid

`balanceOf` and `allowance` Revert on Zero Address Instead of Returning Zero

Description

According to the ERC20 standard and common implementations (OpenZeppelin), balanceOf and allowance view functions should return values for any address input, including address(0). For addresses with no balance or allowance, they should return 0.

The _balanceOf and _allowance functions in ERC20Internals.sol explicitly check for zero addresses and revert instead of returning zero. This deviates from expected ERC20 behavior and can break integrations with external protocols.

// @> In ERC20Internals.sol::_balanceOf (lines 22-38)
function _balanceOf(address owner) internal view returns (uint256) {
assembly {
if iszero(owner) {
revert(0, 0) // @> Reverts with empty data instead of returning 0
}
// ...
}
}
// @> In ERC20Internals.sol::_allowance (lines 72-90)
function _allowance(address owner, address spender) internal view returns (uint256 remaining) {
assembly {
if or(iszero(owner), iszero(spender)) {
revert(0, 0) // @> Reverts with empty data instead of returning 0
}
// ...
}
}

Additionally, the revert uses empty data revert(0, 0) which is inconsistent with other functions in the contract that use proper error selectors (e.g., ERC20InvalidSender, ERC20InvalidReceiver).

Risk

Likelihood: Medium

  • External protocols integrating with this token may call balanceOf(address(0))

  • Some DeFi protocols use balanceOf(address(0)) to track burned tokens

  • Indexers and analytics tools may query edge case addresses

Impact: Low-Medium

  • Integration failures with protocols expecting standard ERC20 behavior

  • Transactions calling balanceOf(address(0)) will unexpectedly revert

  • Silent revert (no error data) makes debugging difficult

  • Deviation from "like openzeppelin implementation" claim in README

Proof of Concept

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;
import {Test, console} from "forge-std/Test.sol";
import {Token} from "./Token.sol";
contract ZeroAddressTest is Test {
Token public token;
function setUp() public {
token = new Token();
}
function test_BalanceOf_ZeroAddress_Reverts() public {
// ERC20 standard: should return 0
// Actual: reverts
vm.expectRevert();
token.balanceOf(address(0));
console.log("balanceOf(address(0)) reverts instead of returning 0");
}
function test_Allowance_ZeroOwner_Reverts() public {
address alice = makeAddr("alice");
// Should return 0, but reverts
vm.expectRevert();
token.allowance(address(0), alice);
}
function test_Allowance_ZeroSpender_Reverts() public {
address alice = makeAddr("alice");
// Should return 0, but reverts
vm.expectRevert();
token.allowance(alice, address(0));
}
// Compare with OpenZeppelin - this would return 0, not revert
function test_OpenZeppelin_Returns_Zero() public pure {
// OpenZeppelin ERC20:
// function balanceOf(address account) public view returns (uint256) {
// return _balances[account];
// }
// For address(0), this returns _balances[address(0)] = 0
console.log("OpenZeppelin would return 0 for balanceOf(address(0))");
}
}

Test Output:

[PASS] test_BalanceOf_ZeroAddress_Reverts() (gas: 13395)
Logs:
CONFIRMED: balanceOf(address(0)) reverts instead of returning 0
[PASS] test_Allowance_ZeroAddress_Reverts() (gas: 19435)
Logs:
CONFIRMED: allowance with zero address reverts

Recommended Mitigation

Remove the zero address checks from _balanceOf and _allowance, allowing them to return the stored value (which would be 0 for addresses with no balance):

function _balanceOf(address owner) internal view returns (uint256) {
assembly {
- if iszero(owner) {
- revert(0, 0)
- }
let baseSlot := _balances.slot
let ptr := mload(0x40)
mstore(ptr, owner)
mstore(add(ptr, 0x20), baseSlot)
let dataSlot := keccak256(ptr, 0x40)
let amount := sload(dataSlot)
mstore(ptr, amount)
mstore(add(ptr, 0x20), 0)
return(ptr, 0x20)
}
}
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)
let baseSlot := _allowances.slot
mstore(ptr, owner)
mstore(add(ptr, 0x20), baseSlot)
let initialHash := keccak256(ptr, 0x40)
mstore(ptr, spender)
mstore(add(ptr, 0x20), initialHash)
let allowanceSlot := keccak256(ptr, 0x40)
remaining := sload(allowanceSlot)
}
}

Alternative: If reverting is intentional, use proper error selectors for consistency:

function _balanceOf(address owner) internal view returns (uint256) {
assembly {
if iszero(owner) {
- revert(0, 0)
+ // ERC20InvalidOwner or custom error
+ mstore(0x00, shl(224, 0x...)) // proper error selector
+ mstore(0x04, owner)
+ revert(0x00, 0x24)
}
// ...
}
}
Updates

Lead Judging Commences

gaurangbrdv Lead Judge 18 days ago
Submission Judgement Published
Validated
Assigned finding tags:

accounting

accounting related issue in token-0x

Support

FAQs

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

Give us feedback!