Three ways to detect if an address is a smart contract
This article describes three methods in Solidity for determining if an address is a smart contract:
- Check if
msg.sender == tx.origin
. This is not a recommended method, but because many smart contracts use it, we discuss this method for completeness. - The second (and recommended way) is to measure the bytecode size of the address using
code.length
. This approach still has limitations that devs must work around. - The third is using
codehash
and is not recommended because it has the same limitations as code.length
with additional complexity.
We discuss each method in this tutorial. Finally, we provide some Solidity puzzles at the end to test your understanding.
Method 1: Using msg.sender == tx.origin to detect if an address is a smart contract
The global variable tx.origin
is the wallet that initiated the transaction, while msg.sender
is the address that called the smart contract. If a wallet calls a smart contract directly, then tx.origin
will be the same as msg.sender
.
However, suppose a wallet calls smart contract A
which then calls smart contract B
.
From contract B
’s perspective, msg.sender
is contract A
and the wallet is tx.origin
. Clearly, msg.sender
will not equal tx.origin
inside of contract B
. The diagram below illustrates the relationship:
By checking if msg.sender == tx.origin
, the smart contract can detect if the incoming call is from a smart contract or from a wallet.
require(msg.sender == tx.origin) is an antipattern
Using a smart contract as a wallet is becoming increasingly popular with the adoption of account abstraction, such as ERC-4337 and using smart contracts for multisignature wallets (like Gnosis Safe).
Adding require(msg.sender == tx.origin)
to a smart contract means that account abstraction wallets and multisignature wallets cannot interact with the smart contract.
This technique can only test if msg.sender
is a contract or not. It cannot test an arbitrary address.
Method 2: Detecting if an address is a smart contract with code.length
The recommended way for a smart contract to test if an address is a smart contract is to measure the size of its bytecode.
If an address has bytecode, then it is a smart contract.
Consider the following code:
contract TestAddress {
function test(
address target
)
public
view
returns (bool isContract) {
if (target.code.length == 0) {
isContract = false;
} else {
isContract = true;
}
}
}
Although all smart contracts have bytecode and all wallet addresses do not, there are some “gotchas” to keep in mind:
- An address which has no bytecode now could have bytecode there in the future if a smart contract gets deployed to that address.
- Using
msg.sender.code.length == 0
is not a reliable way to detect if an incoming call is from a smart contract. If a smart contract makes a call from the constructor then it has not deployed its bytecode yet and msg.sender.code.length
will be 0. While the constructor is executing, the bytecode of the smart contract has not yet been deployed. Therefore, code.length
will be zero. - On EVM chains that support
selfdestruct
, there might have been a smart contract at target
in the past, but the smart contract self destructed.
Testing msg.sender with code.length
If a wallet calls a contract, then msg.sender.code.length
is guaranteed to be 0.
If a contract calls a another contract, then msg.sender.code.length
will be 0 if called from the constructor and non-zero if called from another smart contract function.
Testing an address (not msg.sender) with code.length
If a smart contract uses the address(target).code.length
test on some target
, and the target is a smart contract, then address(target).code.length
is guaranteed to be non-zero.
The dev should keep in mind the code.length
could become 0 later if the contract self destructs (assuming the chain supports selfdestruct and the contract has the ability to selfdestruct).
If a smart contract uses the address(target).code.length
test on some target
, and the target is a wallet, then address(target.code.length)
is guaranteed to be 0.
However, just because address(target).code.length
is 0 now does not mean it will always be zero. A smart contract might be deployed there later. Suppose I give you an address. You measure it now with address(target).code.length
and it returns 0. That measurement will be accurate at the moment you measured it, but it is possible that I could deploy a contract to that address (target
) at a later date and if you measure it again with address(target).code.length
it will be non-zero.
Common use case for checking if an address is a smart contract
If a token is transferred to a smart contract that does not have the functionality to send the tokens out, then the tokens will remain stuck, owned by that contract forever.
As such, some token standards take steps to prevent this from happening.
The ERC-721 with the safeTransferFrom
function for example will check if the address being transferred to is a smart contract (using the code.length
trick).
(original code)
If it is, they attempt to call a special function on the contract to ask if the contract supports ERC-721 tokens. If the function isn’t there, then it knows the tokens will get stuck and it blocks the transfer.
Method 3: Codehash is a bad way to test if an address is a contract
The codehash
returns the keccak256
of the bytecode of an address.
It has the following behavior:
- If the address has no Ethereum balance and no bytecode, there is nothing to hash and returns
bytes32(0)
. - If the address has an Ethereum balance but no bytecode, it returns the keccak256 of empty data
keccak256("")
which equals 0xc5d2460186f7233c927e7db2dcc703c0e500b653ca82273b7bfad8045d85a470
. - If the address has bytecode (regardless of balance) it returns the
keccak256
of the bytecode of the contract.
The exact behavior of codehash is described in the Etherum client comments on codehash.
Some contracts have mistakenly used the codehash
to test if an address has bytecode or not. This is not a good idea because if we use codehash
on a contract that has no bytecode, we will either get back bytes32(0)
or keccak256("")
and we must check both possibilities.
If an address a
has no bytecode and has no ether, then address(a).codehash
returns bytes32(0)
or 32 bytes of all zero. However, if someone transfers Ether to the address, then the codehash
will become keccak256("")
, despite not being a wallet and not a smart contract.
You can test the code below in Remix to see the behavior of codehash:
contract TestHash {
function getHash()
external
view
returns (bytes32) {
// random address with no balance or code
return address(101).codehash;// returns 0x000...000
}
function hashOfNonEmptyWallet()
external
view
returns (bytes32) {
// tx.origin has a non-zero ether balance
return tx.origin.codehash;
// returns a non-zero hash
}
// observe that `keccakNil` and `hashOfNonEmptyWallet`
// return the same value
function keccakNil()
external
pure
returns (bytes32) {
return keccak256("");
}
// Deploy SomeTestContract and put its address in
// codeHashOtherContract to test it
function codeHashOtherContract(
address _a
)
external
view
returns (bool) {
// returns true because the codehash
// of another contract
// is equal to the `keccak256` of its bytecode
return a.codehash == keccak256(a.code);
}
}
contract SomeTestContract {
function someFunction()
external
pure
returns (uint256) {
return 5;
}
}
Both codehash
and code.length
can be used to determine if an address is a smart contract by checking for the presence of bytecode; however, codehash
introduces unnecessary complexity by hashing the bytecode, resulting in three possible outcomes, whereas we only need to check code.length
is zero or not.
It is far simpler to check code.length
.
Puzzle to test your knowledge
Puzzle 1
Can you get the following contract to return true when puzzle
is called and not revert?
contract Puzzle {
function puzzle()
external
view
returns (bool success) {
require(msg.sender != tx.origin);
require(msg.sender.code.length == 0);
success = true;
}
}
Puzzle 2
What should tx.origin.code.length
return? Does it always return the same value?
Learn more with RareSkills
See our Solidity course if you are new to Solidity. See our Solidity bootcamp if you already have some experience. Thanks for reading!
Originally Published April 5, 2024