Root + Impact
Description
-
The ERC-20 standard specifies balanceOf(address) and allowance(address,address) as view functions that return uint256 values without mandating reverts for address(0) inputs. Standard-compliant implementations (OpenZeppelin ERC20) return 0 for uninitialized mapping slots at address(0).
-
In ERC20Internals.sol, internal view functions explicitly revert:
if iszero(owner) {
revert(0, 0)
}
if or(iszero(owner), iszero(spender)) {
revert(0, 0)
}
Risk
Likelihood:
Impact:
-
Integration DoS: Contracts and off-chain processes that expect a 0 result will get a revert, potentially breaking transaction flows.
-
Standard Violation: This deviates from commonly accepted ERC-20 behavior and OpenZeppelin reference implementation.
Proof of Concept
pragma solidity ^0.8.24;
import {Test, console} from "forge-std/Test.sol";
import {ERC20} from "../src/ERC20.sol";
contract ERC20Mock is ERC20 {
constructor() ERC20("Test", "TST") {}
}
contract ComplianceTest is Test {
ERC20Mock public token;
function setUp() public {
token = new ERC20Mock();
}
function test_balanceOf_zeroAddress_reverts() public {
vm.expectRevert();
token.balanceOf(address(0));
}
function test_allowance_zeroAddress_reverts() public {
vm.expectRevert();
token.allowance(address(0), address(1));
}
}
Run:
forge test --match-contract ComplianceTest -vvvv
Output:
[PASS] test_allowance_zeroAddress_reverts() (gas: 9331)
Traces:
[9331] ComplianceTest::test_allowance_zeroAddress_reverts()
├─ [0] VM::expectRevert(custom error 0xf4844814)
│ └─ ← [Return]
├─ [802] ERC20Mock::allowance(0x0000000000000000000000000000000000000000, ECRecover: [0x0000000000000000000000000000000000000001]) [staticcall]
│ └─ ← [Revert] EvmError: Revert
└─ ← [Stop]
[PASS] test_balanceOf_zeroAddress_reverts() (gas: 8904)
Traces:
[8904] ComplianceTest::test_balanceOf_zeroAddress_reverts()
├─ [0] VM::expectRevert(custom error 0xf4844814)
│ └─ ← [Return]
├─ [502] ERC20Mock::balanceOf(0x0000000000000000000000000000000000000000) [staticcall]
│ └─ ← [Revert] EvmError: Revert
└─ ← [Stop]
Recommended Mitigation
function _balanceOf(address owner) internal view returns (uint256) {
assembly {
- if iszero(owner) {
- revert(0, 0)
- }
let baseSlot := _balances.slot
// ... rest of the code
}
}
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)
// ... rest of the code
}
}