Token-0x

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

0 address queries cause unexpected reverts

Author Revealed upon completion

Root + Impact

Description

  • ERC20 view functions like balanceOf() and allowance() typically return 0 for zero address queries as these are non state changing operations that should not revert

  • The implementation explicitly reverts when balanceOf(address(0)) or allowance() with zero addresses are called

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

Risk

Likelihood:

  • Protocols frequently perform balance checks in loops

  • DEX aggregators and portfolio trackers routinely query multiple addresses

Impact:

  • Integration contracts experience unexpected reverts

  • Frontend apps crash when displaying token information

Proof of Concept

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;
import {ERC20} from "./ERC20.sol";
contract VulnerableIntegration is ERC20 {
constructor() ERC20("Test", "TST") {
_mint(msg.sender, 1000e18);
}
function batchCheckBalances(address[] calldata users) external view returns (uint256 total) {
for (uint256 i = 0; i < users.length; i++) {
total += this.balanceOf(users[i]);
}
}
function testZeroAddressDoS() external view {
address[] memory users = new address[](3);
users[0] = address(0x1);
users[1] = address(0);
users[2] = address(0x2);
this.batchCheckBalances(users);
}
}

Recommended Mitigation

function _balanceOf(address owner) internal view returns (uint256) {
assembly {
- if iszero(owner) {
- revert(0, 0)
- }
+ if iszero(owner) {
+ mstore(0x00, 0)
+ return(0x00, 0x20)
+ }
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)
- }
+ if or(iszero(owner), iszero(spender)) {
+ mstore(0x00, 0)
+ return(0x00, 0x20)
+ }
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!