Don’t Use Openzeppelin’s Address.isContract() to Check Caller’s Address
Many projects use openzeppelin’s Address.insContract(address)
to filter invocations from contracts (e.g. to prevent flash loan attacks). However, isContract(address)
is vulnerable when used to identify the caller’s address.
Address.isContract(address)
is often used to determine whether a caller is an EOA( Externally Owned Address) or a contract, and it will return false for the following types of address (according to Address.sol):
- an externally-owned account
- a contract in construction
- an address where a contract will be created
- an address where a contract lived but was destroyed
The third and the fourth ones won’t cause any trouble using isContract
since they are already destroyed or not created yet. However, the second one makes it possible to bypass isContract
, likeisContract(msg.sender)
.
This is one example to test isContract(msg.sender)
:
contract TestAddress {
using Address for address;
function testIsContractCall() public {
require(!Address.isContract(msg.sender)); }
}contract ExternalContractA {
function callTestIsContract(TestAddress addr) public {
addr.testIsContractCall();
}
}contract ExternalContractB {
TestAddress addr;
constructor(TestAddress addr) public {
addr.testIsContractCall();
}
}
The contractTestAddress
is a common contract that uses a require
statement to block Contracts from calling the function testIsContractCall
. If it is called from an EOA, the function call will succeed. However, if it is called from a contract like ExternalContractA
, it will revert.
To bypass the verification of isContract()
, the attacker can invoke testIsContractCall()
in a contract’sconstructor
, like contract ExternalContractB
.
Another situation is also feasible for attackers to bypass the verification even the parameter is not msg.sender
.
contract TestAddress { using Address for address; function testIsContractCall(address to) public { require(!Address.isContract(to)); }}contract ExternalContract { constructor(TestAddressB addr) public { addr.testIsContractCall(address(this)); }}
Although the input of the parameter is to
, which seems not likely to be msg.sender
. However, if this parameterto
can be injected with the caller’s address, it is vulnerable to be bypassed. In this example, the ExternalContract
injects the current address (i.e., address(this)
) into the function call and therefore bypasses the verification.
Some articles may recommend applying require(msg.sender == tx.orign)
to test whether an address is a contract since tx.origin
can never be a contract. This implementation currently works, but Vitalik has publicly stated in 2016 that tx.origin
may not be usable (Now is 2021 Lol). Besides, you need also to be careful of tx.origin
and aware of what it means.
In conclusion, for security considerations, it is not recommended to directly use the return value of Address.isContract()
to determine whether a caller is a contract or not. require(msg.sender == tx.orign)
works now and is a better practice.