文章详细介绍了在Solidity中判断一个地址是否为智能合约的三种方法,包括msg.sender == tx.origin
、code.length
和codehash
,并探讨了每种方法的优缺点和适用场景。
这篇文章描述了在 Solidity 中确定地址是否为智能合约的三种方法:
msg.sender == tx.origin
。这不是推荐的方法,但由于许多智能合约使用它,我们讨论这种方法以保持完整性。code.length
测量地址的字节码大小。这种方法仍然有开发者必须解决的局限性。codehash
,不推荐使用,因为它与 code.length
有相同的局限性,并且复杂度更高。我们将在本教程中讨论每种方法。最后,我们在结尾提供一些 Solidity 难题来测试你的理解。
全局变量 tx.origin
是发起交易的钱包,而 msg.sender
是调用智能合约的地址。如果钱包直接调用智能合约,则 tx.origin
将等于 msg.sender
。
但是,假设一个钱包调用智能合约 A
,然后 A
再调用智能合约 B
。
从 contract B
的角度来看,msg.sender
是 contract A
,而钱包是 tx.origin
。显然,msg.sender
将不等于 tx.origin
,在 contract B
内部。下图说明了这种关系:
通过检查 msg.sender == tx.origin
,智能合约可以检测传入的调用是来自智能合约还是来自钱包。
使用智能合约作为钱包随着账户抽象(例如 ERC-4337 和使用智能合约进行多签名钱包(如 Gnosis Safe)的采用变得越来越流行。
在智能合约中添加 require(msg.sender == tx.origin)
意味着账户抽象钱包和多签名钱包无法与该智能合约进行交互。
这种技巧只能测试 msg.sender
是否为合约,而无法测试任意地址。
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
过去可能存在一个智能合约,但该合约已自毁。_如果一个 钱包 调用一个合约_,则 msg.sender.code.length
保证为 0。
_如果一个 合约 调用另一个合约_,则如果是从构造函数调用 msg.sender.code.length
将为 0,如果是从另一个智能合约函数调用则为非零。
如果一个智能合约在某个 target
上使用 address(target).code.length
测试,并且 target 是一个智能合约,则 address(target).code.length
保证为非零。
开发者应该记住,code.length
以后可能会变为 0 如果合约自毁(假设链支持 selfdestruct 且合约有自毁能力)。
如果一个智能合约在某个 target
上使用 address(target).code.length
测试,并且 target 是一个钱包,则 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代币。如果没有该函数,则知道代币将被困,因而阻止该转移。
codehash
返回地址字节码的 keccak256
。
它具有以下行为:
bytes32(0)
。keccak256("")
的哈希值,即 0xc5d2460186f7233c927e7db2dcc703c0e500b653ca82273b7bfad8045d85a470
。keccak256
。codehash
的确切行为在 以太坊客户端对 codehash 的评论中 有描述。
一些合约错误地使用 codehash
来测试一个地址是否有字节码。这不是一个好主意,因为如果我们在一个没有字节码的合约上使用 codehash
,我们将得到 bytes32(0)
或 keccak256("")
,且我们必须检查这两种可能性。
如果地址 a
没有字节码 并且没有以太,则 address(a).codehash
返回 bytes32(0)
或 32 个全为零的字节。但是,如果有人向该地址转账以太,则 codehash
将变为 keccak256("")
,尽管该地址不是钱包而不是智能合约。
你可以 在 Remix 中测试以下代码 来查看 codehash 的行为:
contract TestHash {
function getHash()
external
view
returns (bytes32) {
// 随机地址,没有余额或代码
return address(101).codehash;// 返回 0x000...000
}
function hashOfNonEmptyWallet()
external
view
returns (bytes32) {
// tx.origin 有非零以太余额
return tx.origin.codehash;
// 返回非零哈希
}
// 观察到 `keccakNil` 和 `hashOfNonEmptyWallet`
// 返回相同的值
function keccakNil()
external
pure
returns (bytes32) {
return keccak256("");
}
// 部署 SomeTestContract 并将其地址放入
// codeHashOtherContract 进行测试
function codeHashOtherContract(
address _a
)
external
view
returns (bool) {
// 返回 true,因为另一个合约的 codehash
// 等于其字节码的 `keccak256`
return a.codehash == keccak256(a.code);
}
}
contract SomeTestContract {
function someFunction()
external
pure
returns (uint256) {
return 5;
}
}
codehash
和 code.length
都可以用来确定一个地址是否为智能合约,通过检查字节码的存在;然而,codehash
通过对字节码进行哈希引入了不必要的复杂性,导致三种可能的结果,而我们只需要检查 code.length
是否为零。
检查 code.length
要简单得多。
你能让以下合约在调用 puzzle
时返回 true 而不导致回溯吗?
contract Puzzle {
function puzzle()
external
view
returns (bool success) {
require(msg.sender != tx.origin);
require(msg.sender.code.length == 0);
success = true;
}
}
tx.origin.code.length
应该返回什么?它总是返回相同的值吗?
如果你是 Solidity 新手,请查看我们的 Solidity 课程。如果你已有一些经验,请查看我们的 Solidity 夏令营。感谢阅读!
最早于 2024年4月5日发布
- 原文链接: rareskills.io/post/solid...
- 登链社区 AI 助手,为大家转译优秀英文文章,如有翻译不通的地方,还请包涵~
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!