SCWE-144: Bypassable Contract Existence Check via extcodesize
Stable Version v1.0
This content is in the version-(v1.0) and still under active development, so it is subject to change any time (e.g. structure, IDs, content, URLs, etc.).
Relationships¶
- CWE-697: Incorrect Comparison
https://cwe.mitre.org/data/definitions/697.html
Description¶
Using extcodesize(addr) > 0 to detect whether an address is a contract fails during construction. When code runs inside a constructor, extcodesize(address(this)) returns 0 because the contract's code is not yet deployed. An attacker can deploy a contract whose constructor calls the victim—the victim sees extcodesize(caller) == 0 and treats the caller as an EOA, bypassing contract-specific restrictions.
Remediation¶
- Do not rely on
extcodesizeto distinguish EOAs from contracts. - If contract-only or EOA-only logic is required, use a different mechanism (e.g., trusted registry, explicit opt-in) or accept that constructor calls cannot be distinguished.
Examples¶
Vulnerable¶
pragma solidity ^0.8.0;
contract TokenGating {
mapping(address => bool) public allowed;
function claim() external {
require(extcodesize(msg.sender) == 0, "Contracts not allowed");
allowed[msg.sender] = true;
}
function extcodesize(address account) internal view returns (uint256 size) {
assembly { size := extcodesize(account) }
}
}
Attacker whose constructor calls claim(). During the constructor, extcodesize(Attacker) == 0, so the check passes and the contract receives the claim.
Fixed¶
pragma solidity ^0.8.0;
contract TokenGating {
mapping(address => bool) public allowed;
mapping(address => bool) public claimed;
function claim() external {
require(allowed[msg.sender], "Not allowed");
require(!claimed[msg.sender], "Already claimed");
claimed[msg.sender] = true;
}
}
extcodesize. Access control is based on explicit registration, not EOA vs contract detection.