Token-0x

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

Calls to `_balanceOf` and `_allowance` with `address(0)` revert instead of returning `0`

Author Revealed upon completion

Description

Under standard ERC20 behavior, querying the balance of an account or the allowance between two addresses is a harmless view operation that never reverts. When address(0) is used, the expected behavior is simply to return 0:

  • balanceOf(address(0))0

  • allowance(address(0), spender)0

  • allowance(owner, address(0))0

In this implementation, both _balanceOf and _allowance revert when given address(0):

  • _balanceOf reverts if owner == address(0)

  • _allowance reverts if owner == address(0) or spender == address(0)

Calls will revert in cases where a normal ERC20 would safely return 0. This breaks expected ERC20 read semantics and can affect integrations or tooling that rely on zero-address queries being safe.

function _balanceOf(address owner) internal view returns (uint256) {
assembly {
// @audit low - shouldn't revert
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 {
// @audit low - shouldn't revert
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)
}
}

Risk

Likelihood: Medium

Zero-address balance and allowance reads occur in both on-chain validation paths and off-chain tooling. Because ERC20s normally return 0 for these cases, unexpected reverts can to appear during real integrations.

Impact: Low

The issue does not threaten funds or core token functionality, but it does cause unnecessary reverts in systems that assume ERC20-standard behavior. This reduces compatibility and may require special-case handling for this token.

Recommended Mitigation

Remove the zero-address checks in _balanceOf and _allowance.

The _balances mapping is never updated for address(0) in _burn and _transfer reverts if it is to or from address(0), so it will always return 0.

Similarly, _approve reverts when owner or spender is address(0), so _allowance can safely rely on the default mapping value and will return 0 when either is address(0).

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!