三种检测地址是否为智能合约的方法

文章详细介绍了在Solidity中判断一个地址是否为智能合约的三种方法,包括msg.sender == tx.origincode.lengthcodehash,并探讨了每种方法的优缺点和适用场景。

这篇文章描述了在 Solidity 中确定地址是否为智能合约的三种方法:

  • 检查 msg.sender == tx.origin。这不是推荐的方法,但由于许多智能合约使用它,我们讨论这种方法以保持完整性。
  • 第二种(也是推荐的方法)是使用 code.length 测量地址的字节码大小。这种方法仍然有开发者必须解决的局限性。
  • 第三种是使用 codehash,不推荐使用,因为它与 code.length 有相同的局限性,并且复杂度更高。

我们将在本教程中讨论每种方法。最后,我们在结尾提供一些 Solidity 难题来测试你的理解。

方法 1:使用 msg.sender == tx.origin 检测地址是否为智能合约

全局变量 tx.origin 是发起交易的钱包,而 msg.sender 是调用智能合约的地址。如果钱包直接调用智能合约,则 tx.origin 将等于 msg.sender

但是,假设一个钱包调用智能合约 A,然后 A 再调用智能合约 B

contract B 的角度来看,msg.sendercontract A,而钱包是 tx.origin。显然,msg.sender 将不等于 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 测试,并且 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 技巧)。

一个高亮显示了 code.length 的 if 语句的 checkOnERC721Received 函数代码片段

(原始代码)

如果是,他们会尝试调用合约上的特殊函数,询问该合约是否支持ERC-721代币。如果没有该函数,则知道代币将被困,因而阻止该转移。

方法 3:使用 codehash 测试地址是否为合约的糟糕方法

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;    
    }
}

codehashcode.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日发布

  • 原文链接: rareskills.io/post/solid...
  • 登链社区 AI 助手,为大家转译优秀英文文章,如有翻译不通的地方,还请包涵~
点赞 0
收藏 0
分享
本文参与登链社区写作激励计划 ,好文好收益,欢迎正在阅读的你也加入。

0 条评论

请先 登录 后评论
RareSkills
RareSkills
https://www.rareskills.io/