Securing Smart Contracts: The extcodesize Pitfall
A common method of checking EOA or a contract address, extcodesize function, has a vulnerability. Attackers can exploit it during contract construction, allowing them to bypass check and attack smart contracts. Be aware of this pitfall and take extra measures to secure them.
Why we should not use extcodesize to checks?
There are several codebases implemented in the solidity that need a check to verify whether a call was made from an externally owned account or a contract account.
In this post, I explain some potential issues that might mislead the contract logic.
Know about the addresses in the Ethereum blockchain, there are two types of addresses:
- Externally Owned Accounts (EOA): These are addresses controlled by private keys and are typically used to store and transfer Ether and other assets. (eg: wallet addresses)
- Contract Addresses: These are addresses that represent smart contracts on the Ethereum blockchain. They can be used to store and transfer assets as well as execute code.
extcodesize is like the "weight" function for contracts on the Ethereum blockchain. Just like how you weigh an object to know how heavy it is, you use extcodesize to know how "big" a contract is, measured in bytes. In short, it returns the size of the contract.
When the size of an address
addr is equal to zero, it indicates that the
addr is an externally owned account (EOA) and does not contain code. However, if the size is not zero, the addr is a contract address.
However, this approach has a vulnerability. When a contract is being constructed, it does not have source code available, so while the constructor is running, it can make calls to other contracts but
extcodesize for its address will return zero. This means that an attacker can create a contract that calls another contract and bypass the check.
Try to spot the bug
- The contract, called "Blocklist," has a mapping of addresses to booleans that keeps track of the blocklisted addresses.
- The contract has an address variable called "manager" that is public and is set in the constructor function.
- The "block" function allows the manager to add a contract address to the blocklist, but only if the address is a smart contract.
- The "isBlocked" function allows anyone to check if a given contract address is on the blocklist.
- The "_isContract" function is an internal function that checks if the given address is a smart contract by checking the size of the code deployed on the address
- The constructor function is called when the contract is first deployed, it takes manager and VotingEscrow contract address as parameter and set the manager address.
The sequence of the actual attack is as follows:
- The attacker creates a new contract that they want to add to the blocklist, but this contract has a constructor that is currently executing.
- The attacker calls the "block" function and passes in the address of the contract with the executing constructor.
- The first require statement in the block function passes since the message sender is the manager.
- The block function calls the "_isContract" function, passing in the address of the contract with the executing constructor.
- Since the constructor is currently executing, the
extcodesize(addr)function will return zero, and the _isContract function will return false.
- The second require statement in the block function fails, and the function reverts, preventing the attacker from adding the contract to the blocklist.
addrisn't added to
The attacker succeeded to bypass the block.
It is important to be aware of this pitfall and implement additional security measures to protect smart contracts from potential attacks while using
extcodesize (Inline assembly method) or
address.code.length (Solidity >= 0.8.0)
The above example is from fiatdao contest from code4rena by JohnSmith. The severity varies depending on the codebase.
Here are more accepted risks of the vulnerability 2022-04-jpegd
That's a great read. This is Jagadesh, and you can find me on twitter, linkedin. See you next time.