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