如何判断以太坊地址是否为合约地址?

37

在Solidity中,地址可以是账户、合约(或者其他一些东西,例如交易)。当我有一个变量x,保存了一个地址,我该如何测试它是否为合约?

(是的,我已经阅读了文档中关于类型的章节)

8个回答

63

是的,您可以通过使用一些EVM汇编代码来获取地址的代码大小:

function isContract(address addr) returns (bool) {
  uint size;
  assembly { size := extcodesize(addr) }
  return size > 0;
}

3
以下是这个函数如何工作的一些信息 - manidos
17
这段代码很危险,因为在合约构造函数中,EXTCODESIZE将返回0,所以它已经不再被建议使用。 - eth

33

使用EXTCODESIZE函数的最高票答案被发现可以被黑客攻击。

如果该函数从合约的构造函数中调用(因为合约尚未部署),它将返回false。

应非常小心地使用此代码,以避免安全漏洞,例如:

https://www.reddit.com/r/ethereum/comments/916xni/how_to_pwn_fomo3d_a_beginners_guide (存档)

重复强调:

不要使用EXTCODESIZE检查来防止智能合约调用函数。这是不可靠的,它可以通过构造函数调用来破坏,因为当构造函数运行时,该地址的EXTCODESIZE返回0。

请参见示例代码,了解欺骗EXTCODESIZE返回0的合约。


检查调用者是否为合约

如果您想确保一个EOA正在调用您的合约,简单的方法是require(msg.sender == tx.origin)。但是,防止一个合约是一种反模式,会涉及到安全性和互操作性问题。具体请参考此处以及这里这里

当账户抽象化实现时,require(msg.sender == tx.origin)需要重新审视。

检查被调用方是否是一个合约

正如@Luke在评论中指出的那样,没有通用的链上方法可以了解有关被调用方的信息。如果要“调用”地址,则没有一般性的方法可用于确定该地址是合约、EOA还是可部署新合约的地址,或者它是否是CREATE2地址。

有一种针对某些被调用方的非通用方法:您可以在链上拥有一个映射,存储已知的EOA或合约地址。(请记住,对于没有任何链上历史记录的地址,您无法知道它是EOA还是可以部署合约的地址。)


3
需要指出的是,require(msg.sender == tx.origin) 只能检测函数调用者是否为 EOA,不能用于检测任何其他第三方合约是否为 EOA(例如您想从自己的函数中调用的合约)。 - Luke Hutchison
1
@LukeHutchison 点赞,非常好的观点!添加了调用者和被调用者的情况;如果我漏掉了一些东西或者你有其他建议,很高兴能听到你的意见。 - eth
1
为了完整起见,你可以补充说明在Solidity中,extcodesize现在被抽象化为<address>.code.size(不需要汇编语言)。人们需要认识到这种形式。(我可能有语法错误,因为我现在离电脑很远。) - Luke Hutchison

14

你不能在Solidity合约内查询此信息,但是如果你只想知道一个地址是否拥有合约代码,你可以使用geth控制台或类似工具进行检查,例如:

  > eth.getCode("0xbfb2e296d9cf3e593e79981235aed29ab9984c0f")

使用十六进制字符串(这里是0xbfb2e296d9cf3e593e79981235aed29ab9984c0f)作为您要查询的地址。这将返回存储在该地址处的字节码。

您也可以使用区块链扫描器找到该地址处合约的源代码,例如在etherscan.io上显示的ecsol库


11
编辑:自这个答案最初撰写以来,Solidity 已经发生了变化,@manuel-aráoz 给出了正确的答案。
在 Solidity 中没有办法检查一个地址是否是合约。以太坊的目标之一是让人类和智能合约平等对待。这将导致一个未来,在这个未来中,智能合约与人类和其他合约无缝地互动。虽然这种情况可能会在未来发生变化,但目前一个任意地址是不明确的。

5
发送到合约所消耗的燃气与发送到地址所消耗的燃气是完全不同的。如果有一个目标要以相同方式处理这两件事情,那么就无法进行燃气区分。 - James Moore

2
如果你想使用nodejs进行确认,可以这样做:
const Web3 = require('web3')

// make sure you are running geth locally
const web3 = new Web3(new Web3.providers.HttpProvider('http://localhost:8545'))

is_contract = async function(address) {
    res = await web3.eth.getCode(address)
    return res.length > 5
}

is_contract('your address').then(console.log)

1
我不认为这会有太大的区别,但我很好奇你为什么选择了 return res.length > 5?如果这不是一个智能合约,那么 res 不应该是 0x 吗?这意味着 res.length > 2 也同样有效?我想你也可以测试 res.startsWith("0x6080604052") - Matthew Scerri

2

从openzeppeling的Address.sol库中,它有这个函数:

pragma solidity ^0.8.1;

function isContract(address account) internal view returns (bool) {
    // This method relies on extcodesize/address.code.length, which returns 0
    // for contracts in construction, since the code is only stored at the end
    // of the constructor execution.

    return account.code.length > 0;
}

isContract函数对以下类型的地址将返回false:

  • 外部拥有的账户

  • 正在构建中的合约

  • 将要创建合约的地址

  • 曾经存在过的合约地址,但已被销毁


-1

如果您掌握了相关信息,您可以做什么。 如果交易发送者地址为空或未被占用,则可以确定该地址是合约账户还是EOA(外部拥有的账户)。 例如,在网络上发送创建合约交易时,交易中的接收地址为空/未使用。

参考自Github: https://github.com/ethereum/go-ethereum/wiki/Contracts-and-Transactions

希望这能帮到您。


这个链接显然已经过时了。现在是2021年,而这个答案是2016年的。 - Malone

-1
如果您正在检查“调用者”是否是EOA而不是合约:
简短回答:
require(tx.origin == msg.sender);

tx.origin是指发起此连续函数调用的原始地址的引用,而msg.sender是直接调用目标函数的地址。这意味着,tx.origin必须是人类,而msg.sender可以是合约或人类。因此,如果有人从合约中调用您,那么msg.sender就是一个合约地址,与tx.origin不同。

我知道大多数合约可能会使用@Manuel Aráoz的代码,在大多数情况下都能正常工作。但是,如果您在合约的构造函数中调用函数,则extcodesize将返回0,这将导致isContract检查失败。

注意:如果您不清楚tx.origin代表什么,请勿在其他情况下使用它。


这意味着直接用户调用函数。这是缓解可重入攻击的一种方式,因为如果(tx.origin == msg.sender),则意味着没有函数调用链。 - Yilmaz

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接