Token-0x

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

`balanceOf(0)` and `allowance` With Zero Addresses Revert Instead of Returning 0

Author Revealed upon completion

Description:

The internal view functions _balanceOf and _allowance are implemented in Yul and explicitly revert when called with zero addresses:

function _balanceOf(address owner) internal view returns (uint256) {
assembly {
if iszero(owner) {
revert(0, 0)
}
// ...
}
}
function _allowance(address owner, address spender) internal view returns (uint256 remaining) {
assembly {
if or(iszero(owner), iszero(spender)) {
revert(0, 0)
}
// ...
}
}

Since the public balanceOf and allowance functions are thin wrappers:

function balanceOf(address owner) public view virtual returns (uint256) {
return _balanceOf(owner);
}
function allowance(address owner, address spender) public view virtual returns (uint256) {
return _allowance(owner, spender);
}

calls like balanceOf(address(0)) and allowance(address(0), someSpender) will revert with an empty error.

By convention, and in most widely used ERC20s (including OpenZeppelin), these calls simply return 0. Many tools and protocols rely on being able to call balanceOf(0x0) to track minted tokens, or allowance(0x0, X) without expecting a revert.

Impact:

  • Breaks compatibility with code that assumes balanceOf(address(0)) and allowance with zero addresses are well‑defined and return 0.

  • Can cause unexpected reverts in third‑party integrations, indexers, analytics tools, or even other contracts.

  • This makes Token-0x not a strict drop‑in replacement for OpenZeppelin ERC20 from a behavioral standpoint.

Proof of Concept:

Token using Token-0x ERC20:

// Add this test to Token.t.sol
function test_balanceOfZeroAddressReverts() public {
vm.expectRevert();
token.balanceOf(address(0));
}
function test_allowanceWithZeroAddressesReverts() public {
address spender = makeAddr("spender");
vm.expectRevert();
token.allowance(address(0), spender);
vm.expectRevert();
token.allowance(makeAddr("owner"), address(0));
}

Token2 using OpenZeppelin ERC20:

// Add this test to Token2.t.sol
function test_balanceOfZeroAddressReturnsZero_OZ() public {
assertEq(token.balanceOf(address(0)), 0);
}
function test_allowanceWithZeroAddressesReturnsZero_OZ() public {
address owner = makeAddr("owner");
address spender = makeAddr("spender");
assertEq(token.allowance(address(0), spender), 0);
assertEq(token.allowance(owner, address(0)), 0);
}

These tests show that Token-0x reverts where OpenZeppelin returns 0.

Mitigation:

  • Align behavior with the de facto standard by removing the zero-address revert in _balanceOf and _allowance, and simply returning the stored value (which will be zero for unused slots):

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)
}
}

Support

FAQs

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

Give us feedback!