Understanding Solidity Assembly: Checking if an Address is a Smart Contract
Knowing if an address has a smart contract living at it can be useful for a number of reasons. For example, the ERC721Receiver interface specifies a method that ERC-721 NFTs should call to ensure that a smart contract can safely receive the tokens. But the NFT developer also wants tokens to be receivable by externally owned accounts (EOA’s, like addresses generated through MetaMask) and thus must first know whether the recipient address is indeed a smart contract before calling onERC721Received
.
The key insight to solve this problem is the knowledge that EOAs have no code associated with them, while smart contracts have some amount of code. Thus, just knowing how much code lives at an address will answer the question without needing any more specifics about the code itself. To find the code length, you can use the built-in code.length
function.
address.code.length > 0;
This is the same method that OpenZeppelin uses in their isContract
function. But Assembly also has a built-in function called extcodesize.
function checkSize(address addr) public view returns(uint extSize) {
assembly {
extSize := extcodesize(addr) // returns 0 if EOA, >0 if smart contract
}
}
Using pure Assembly is slightly more gas efficient, using 24,683 versus 24,711.
Quick Note on Security
Checking an address for code size is useful if it is being done for the benefit of the user, such as preventing mistaken transfers of valuable NFTs to contracts where they could be locked forever. However, it should not be relied upon for functions that absolutely require the caller to not be a smart contract for security purposes. This is because returns 0 if it is called from the constructor of a contract, so an attacker could bypass the check by placing the function call in the constructor. Both this warning and the exploit code snippet below are credited to StackExchange:
contract Victim {
function isContract() public view returns(bool){
uint32 size;
address a = msg.sender;
assembly {
size := extcodesize(a)
}
return (size > 0);
}
}
contract Attacker {
bool public iTrickedIt;
Victim v;
constructor(address _v) public {
v = Victim(_v);
// addrss(this) doesn't have code, yet
iTrickedIt = !v.isContract();
}
}