本文介绍了在 Solidity 中判断一个地址是否为智能合约的三种方法:
- 检查
msg.sender == tx.origin。这并不是推荐的方法,但由于许多智能合约都在使用它,为了全面起见,我们还是会讨论这种方法。 - 第二种(也是推荐的做法)是使用
code.length测量该地址的字节码大小。这种方法仍然存在一些开发者必须设法规避的局限性。 - 第三种是使用
codehash,不推荐使用这种方法,因为它具有与code.length相同的局限性,并且增加了额外的复杂性。
我们将在本教程中讨论每种方法。最后,我们会在文末提供一些 Solidity 谜题来测试你的理解程度。
方法 1:使用 msg.sender == tx.origin 检测地址是否为智能合约
全局变量 tx.origin 是发起交易的钱包,而 msg.sender 是调用智能合约的地址。如果钱包直接调用智能合约,那么 tx.origin 将与 msg.sender 相同。
但是,假设一个钱包调用了智能合约 contract A,然后它又调用了智能合约 contract B。
从 contract B 的角度来看,msg.sender 是 contract A,而钱包是 tx.origin。显然,在 contract B 内部 msg.sender 将不等于 tx.origin。下图说明了这种关系:

通过检查 msg.sender == tx.origin,智能合约可以检测传入的调用是来自智能合约还是来自钱包。
require(msg.sender == tx.origin) 是一种反模式
随着账户抽象(如 ERC-4337)的采用以及将智能合约用于多签钱包(如 Gnosis Safe),使用智能合约作为钱包变得越来越流行。
在智能合约中添加 require(msg.sender == tx.origin) 意味着账户抽象钱包和多签钱包将无法与该智能合约进行交互。
这种技术只能测试 msg.sender 是否为合约。它无法测试任意地址。
方法 2:使用 code.length 检测地址是否为智能合约
智能合约测试一个地址是否为智能合约的推荐方法是测量其字节码的大小。
如果一个地址有字节码,那么它就是一个智能合约。
考虑以下代码:
contract TestAddress {
function test(
address target
)
public
view
returns (bool isContract) {
if (target.code.length == 0) {
isContract = false;
} else {
isContract = true;
}
}
}
尽管所有的智能合约都有字节码,而所有的钱包地址都没有,但仍有一些需要注意的“坑”:
- 一个现在没有字节码的地址,如果在未来有智能合约部署到该地址,那么它就会产生字节码。
- 使用
msg.sender.code.length == 0来检测 传入 调用是否来自智能合约并不是一种可靠的方法。如果智能合约 从构造函数 发起调用,那么它的字节码还没有被部署,此时msg.sender.code.length将为 0。在构造函数执行期间,智能合约的字节码尚未部署。因此,code.length会是零。 - 在支持
selfdestruct的 EVM 链上,过去可能在target处存在过智能合约,但该智能合约后来被自毁了。
使用 code.length 测试 msg.sender
如果一个 钱包 调用了一个合约,那么 msg.sender.code.length 保证为 0。
如果一个 合约 调用了另一个合约,当从构造函数中调用时,msg.sender.code.length 将为 0;如果从其他智能合约函数中调用,则为非零。
使用 code.length 测试地址(非 msg.sender)
如果智能合约对某个 target 使用 address(target).code.length 测试,且该目标是一个智能合约,那么 address(target).code.length 保证为非零。
开发者应牢记,如果合约发生自毁(假设该链支持 selfdestruct 且合约具备自毁能力),code.length 随后可能会变成 0。
如果智能合约对某个 target 使用 address(target).code.length 测试,且该目标是一个钱包,那么 address(target.code.length) 保证为 0。
然而,仅仅因为目前 address(target).code.length 为 0,并不意味着它将永远为零。稍后可能会有智能合约部署在此处。假设我给你一个地址。你现在用 address(target).code.length 测量它并返回 0。该测量在测量的那一刻是准确的,但我有可能在日后将一个合约部署到该地址(target),如果你再次用 address(target).code.length 测量它,它将变为非零。
检查地址是否为智能合约的常见用例
如果将代币转移到一个不具备发送代币功能的智能合约中,那么这些代币将会被困住,并永远归该合约所有。
因此,一些代币标准采取了措施来防止这种情况发生。
例如,带有 safeTransferFrom 函数的 ERC-721 标准将会检查接收地址是否为一个智能合约(使用了 code.length 技巧)。

(源代码)
如果是的话,它们会尝试调用该合约上的一个特殊函数来询问合约是否支持 ERC-721 代币。如果该函数不存在,那么它就会知道代币将会被困住,从而阻止这次转移。
方法 3:Codehash 是一种测试地址是否为合约的糟糕方法
codehash 会返回地址字节码的 keccak256 哈希值。
它的行为如下:
- 如果该地址既没有以太币余额也没有字节码,则没有什么可进行哈希的,它会返回
bytes32(0)。 - 如果该地址有以太币余额但没有字节码,它会返回空数据的
keccak256哈希值keccak256(""),即0xc5d2460186f7233c927e7db2dcc703c0e500b653ca82273b7bfad8045d85a470。 - 如果该地址有字节码(不论余额多少),它会返回该合约字节码的
keccak256哈希值。
codehash 的确切行为在 关于 codehash 的 Ethereum 客户端注释 中进行了描述。
有些合约错误地使用了 codehash 来测试一个地址是否有字节码。这并不是一个好主意,因为如果我们对一个没有字节码的合约使用 codehash,我们将得到 bytes32(0) 或者 keccak256(""),而我们必须同时检查这两种可能性。
如果地址 a 没有字节码 且没有以太币,那么 address(a).codehash 会返回 bytes32(0) 即全零的 32 字节。然而,如果有人向该地址转账了以太币,那么其 codehash 将变为 keccak256(""),尽管它既不是钱包也不是智能合约。
你可以在 Remix 中测试以下代码 以观察 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;
}
}
codehash 和 code.length 都可以通过检查是否存在字节码来判断一个地址是否为智能合约;然而,codehash 因为对字节码进行哈希处理而引入了不必要的复杂性,导致可能出现三种结果,而我们其实只需要检查 code.length 是否为零即可。
检查 code.length 要简单得多。
测试你知识的谜题
谜题 1
你能让以下合约在调用 puzzle 时返回 true 并且不发生回滚吗?
contract Puzzle {
function puzzle()
external
view
returns (bool success) {
require(msg.sender != tx.origin);
require(msg.sender.code.length == 0);
success = true;
}
}
谜题 2
tx.origin.code.length 应该返回什么?它总是返回相同的值吗?
在 RareSkills 了解更多
如果你是 Solidity 新手,请查看我们的 Solidity 课程。如果你已经有一些经验,请查看我们的 Solidity 训练营。感谢阅读!
最初发布于 2024 年 4 月 5 日