您正在查看: Ethereum-优秀转载 分类下的文章

分析0xSplits - 收入拆分协议

技术背景

很多时候,我们需要把一份收入(金额)拆分到不同的地址,而又不想让付款人(通常是用户)承担拆分收入部分的 GAS费用, 0xSplits 的架构及解决思路值得我们参考。
本文先介绍了0xSplits 是什么,0xSplits 采用的架构是怎么样的,同时介绍了几种其他的拆分方式可能带来的问题,另外还有一些0xSplits在 Gas 优化上的做法。

0xSplits是什么

0xSplits 是链上收入拆分的协议, 它是一个智能合约,允许你将任何付款拆分到许多收款人,例如,它可以用于NFT版税。EIP-2918允许你只指定一个地址作为收款人,但如果你把这个地址设置为0xSplits地址,你就可以有很多个收款人。
0xSplits由一个主合约组成,可以创建许多子克隆合约(SplitWallet)。

  1. 你通过调用主合约创建一个新的拆分(split)
  2. 它创建一个负责接收ETH的子合约
  3. 在需要接收ETH的地方使用子合约的地址

译者注:克隆合约是使用同一份代码,创建(复制)多个出合约, 每个合约都有自己的状态。

这里是子克隆合约的代码,你可以看到,它非常简单。它可以接收ETH,并且有一个函数可以将ETH转移到主合约, 0xSplits也支持ERC-20,但省略了这部分代码

但收到的ETH如何拆分给接收者?这就是主合约的任务(为了便于阅读,代码做了修改):


所以,资金是静止不动的,直到有人明确地调用 distributeETH。但为什么有人会浪费Gas来调用这个函数呢?答案是distributorFee(分配费用)。这是在创建拆分时设置的,谁调用distributionETH就归谁。

这种激励性的代码调用是必要的,因为EVM没有周期任务(cron )或回调来定期分配资金,一切都需要由个人或链外机器人发起,distributorFee有点类似于CryptoKitties合约中的autoBirthFee。

为什么0xSplits会选择这种架构?还有,为什么不在收到资金的时候立即分发?这是我们这篇文章的主要想解决的问题。

0xSplits 架构

你可以在Etherscan找到0xSplits的代码

克隆SplitWallet合约接收付款并持有资金,直到它们被调用,将资金转移到主合约。

分销商(distributor)被激励在主合约中调用distributeEth,这将把资金从克隆合约转移到主合约中,同时也将支付/分成。

但是在这一点上,资金仍然存放在主合约中(合约有一个从收款人地址到余额的映射)。为了实际接收资金,用户必须调用主合约中的withdraw函数。

此架构的可能替代方案
现有的设定是相当复杂的:克隆合约接收付款,分销商(distributor)将这些资金从克隆合约转移到主合约,并将资金分给接收者。但是收款人仍然需要调用withdraw来接收实际拆分到的资金。所以,端到端的用户流程并不是完全自动化的(另外,有些钱会被分销商收取)。

为什么0xSplits需要如此复杂的设置?让我们来看看其他的方法,以及它们为什么不可行。

若在收到资金时进行拆分?

为什么不在收到资金的同一函数中进行拆分呢?也就是说,不在合约中保存资金, 而是立即分配,这里有2个原因:

  1. 付款人需要提供额外的Gas
  2. 它破坏了互操作性。付款人不能再直接将ETH转账(transfer)到克隆合约中。而是要求付款人将不得不调用合约上的一个特定函数(来进行拆分),这会破坏互操作性。你可能会说只要重写receive函数,但receive函数经常受2300个Gas限制,这意味着你不能在其函数中进行大量的转账。

若使用共享合约而不是克隆合约呢?

为什么没有一个共享的合约,为所有可能的拆分接收所有的付款?这将摆脱克隆(创建新合约),但它并不奏效,因为当共享合约收到付款时,它不知道付款是要进行怎样的拆分。

将distributeEth和withdraw合二为一呢?

为什么不把整个过程端到端自动化,让分销商在一个函数中执行这2个操作?主要是因为安全问题。强烈建议使用pull over push - 即让用户手动拉取他们的自己资金,而不是把资金自动推送他们。

为什么 "推(push)"被认为是一种安全风险?阅读下面的链接以获得完整的答案,但简而言之,这是因为不是所有的接收者都能保证正确处理ETH的接收。一些恶意的行为者可能会部署一个智能合约,在 receive 功能中进行回退(revert)。因此,如果至少有一个接收者回退(revert),整个操作就会回退(revert)。
参考阅读文章:Pull over Push 将转移以太坊的相关风险转移到用户身上。

如果你想出另一种替代架构,请告诉我(在评论中这里)。

我认为0xSplits为他们所针对的使用场景选择了正确的架构--这就是成为DeFi生态系统中的一个基本构件。还有其他类似的支付拆分器,它们是针对不同的使用场景,它们使用不同的架构。例如,disperse.app在收到资金时进行拆分,它可能是一个更好的一次性付款拆分工具。

0xSplits 代码

我对合约进行了重组,并将所有内容按功能分组,以方便阅读。
现在我们来看看现有的架构是如何在代码中实现的。
这里只有2个合约:SplitMain.sol和SplitWallet.sol(克隆)。其余的是库和接口。
你已经在前面看到了SplitWallet.sol的代码,但我把再次复制到这里只是为了参考。

这很简单。它可以接收ETH并将资金转账到主合约。你可能会问,如果没有 receive 功能,它如何接收ETH?答案是,"克隆 "库创建该合约的克隆,神奇地插入 receive 功能的汇编代码。

现在到了主合约 - SplitMain.sol, 这是所有行动发生的地方。

创建一个拆分合约

SplitMain.sol合约开头的函数,用于创建新的拆分。

validSplit只是验证一些东西,如:

  • 百分比的总和应该是1。
  • 收款人和百分比数组具有相同的长度。
  • 收款人地址数组需要被排序。为什么?我们很快就会知道。

如果controller是零地址,这意味着拆分没有所有者,它变得不可改变。Clones库将创建一个SplitWallet合约的克隆,并保存在构造函数中。

clone和cloneDeterministic(在上面的createSplit函数中)的区别是,确定性的部署到一个预先定义的地址(由传入的splitHash决定)。不可变(Immutable)的拆分合约使用确定性的克隆,以避免有人创建完全相同的拆分合约时时发生碰撞。

拆分(split)是由这些数据结构表示的:

使用哈希值的Gas优化

请注意,上面只保存了拆分的哈希值,而没有保存地址、收款人和distributorFee。为什么我们需要哈希值?

哈希值是用来将所有关于拆分的信息(收款人、百分比、distributorFee)合成一个字符串。

通过只存储哈希值而不是函数的所有参数,我们节省了大量的存储空间,从而节省了Gas。

但是,我们如何查找在哈希过程中丢失的信息,如recipients?我们要求这些信息被传递到需要它们的函数中。在合约中,我们只需再次对传入的参数进行散列,并与存储的散列进行比较。如果它们匹配,传入的参数就是正确的。

分销商被激励在链外记录recipients, percentages等,并将这些信息传递给所有需要它们的函数。分销商费用则是支付他们的服务的费用。

另外,我们现在明白为什么收款人地址数组需要在createSplit函数中进行排序。因为否则,哈希值将无法重现。

更新拆分的内容

用哈希值更新拆分也变得非常有效。只要更新哈希值即可。

(onlySplitController确保msg.sender == split.controller)

所有权的转移

如果一个拆分是可变的,你可以转移其所有权。

这是有两步过程:

为什么是两步程序?为了防止意外转移到一个错误的地址。两步程序使你的合约更安全一些(代价是略微多一点Gas)。

资金如何拆分分配

资金是如何分配的?让我们来看看:

我们首先通过哈希算法验证传入的args,并与存储的哈希算法进行比较。然后我们将资金转移到主合约,为分销商预留奖励,最后分配资金。

这个函数为了可读性做了大量的修改。请阅读原始的源代码以了解实际的实现。

终端用户提取资金

主合约的最后一个功能是收款人提取资金的能力。这是一个非常简单的功能。


由于ethBalances中的资金来于所有的克隆拆分合约。但是withdraw需要手动调用,机器人/分销商没有受到激励进行这个动作。

同样有趣的是,有人可以代表你调用withdraw(为你的Gas付钱)。

其他的想法

  • 0xSplits 实际上允许你有嵌套的拆分--指定一个拆分为另一个拆分的收款人。
  • 0xSplits 也适用于 ERC-20 资金的拆分。我只是省略了代码以方便阅读。
  • 不小心发送到主合约的资金是无法恢复的,因为没有办法提取多余的ETH。
  • OpenZeppelin也有一个支付拆分器,但我还没有研究过它是如何实现的,还有disperse.app,对于一次性的拆分来说可能更好。
  • 用比特币而不是用以太坊来拆分支付要容易得多。由于UTXO架构的存在,比特币几乎拥有这种开箱即用的功能。(见 Solidity Fridays)

就这样吧!让我知道你对0xSplits的看法,在评论区

转载:https://learnblockchain.cn/article/5560

Solidity Gas 优化 - 理解不同变量 Gas 差异

本文我们来尝试进行 RareSkills Gas 优化的一个挑战,完成这个挑战可以学习到更多 Gas 技巧。
以下是 Distribute 合约,合约的作用是向 4 个贡献者平均分配 1 ETH, 代码如下

// SPDX-License-Identifier: GPL-3.0
pragma solidity 0.8.15;

contract Distribute {
    address[4] public contributors;
    uint256 public createTime;

    constructor(address[4] memory _contributors) payable {
        contributors = _contributors;
        createTime = block.timestamp;
    }

    function distribute() external {
        require(
            block.timestamp > createTime + 1 weeks,
            "cannot distribute yet"
        );

        uint256 amount = address(this).balance / 4;
        payable(contributors[0]).transfer(amount);
        payable(contributors[1]).transfer(amount);
        payable(contributors[2]).transfer(amount);
        payable(contributors[3]).transfer(amount);
    }
}

合约代码:https://github.com/RareSkills/gas-puzzles/blob/main/contracts/Distribute.sol

这个合约默认情况下,所需 GAS 是 71953, 需要优化到 57044, 以便通过测试用例,测试用例代码在这里。
这是默认运行的截图:

在原始合约上运行测试结果

挑战的要求是仅修改合约Solidity代码来使 distribute 下降到57044, 因此不可以修改优化器或solidity版本、不可以修改测试文件,也不使用huff/yul/bytecode 层面的优化,也不变更函数属性(不能使函数为 "payable")。
在进一步阅读之前,请确保你自己先尝试解决这个难题,它真的很有趣,而且能让你学到东西。

让我们来优化吧!

让我们先做一些明显的事情。

  • 变更变量为不可变(immutable)变量,而不是存储变量(节省 10710Gas--因为不可变变量是作为常量嵌入合约字节码的,而不是从存储中读取)。
  • 用 send 取代 transfer(节省108 Gas--因为send没有检查转账是否成功)。
  • 用endTime代替currentTime (节省了69个Gas - 在构造函数中进行时间计算)

以下是修改后的合约:

这样我们就可以让Gas 节约到61066,已经比原来的好了10887,但仍比目标值高出 4千, 需继续努力。

那么,还有什么诀窍呢?

这个挑战有一个特别的技巧,应该教给你。而这是一种通过SELFDESTRUCT向地址发送ETH的老方法:

通过Selfdestruct 解谜, 仅仅这一招就为你节省了4066*的Gas,并且达到了目标!

但如果继续深入呢?

每一个理智的优化总是有其疯狂的邪恶兄弟(作弊)......让我们看看我们能把这个推到什么程度!
让合约自毁(selfdestruct)感觉像是在作弊 -- 为什么你会有一个这样的合约,在第一次调用分发后就不存在了呢?不过测试通过,所以我想这是允许的......?

让我们看看还有什么(真正的作弊)能让测试通过,但又能优化更多Gas。

  • 让contributors成为常数,而不是不可变(immutable)(节省24个Gas - 因为Hardhat地址总是相同的,所以可以这样做,对吧?
  • 使金额恒定为0.25ETH,而不是从余额中计算(节省了106个Gas -- 因为在测试中金额总是相同的,所以为什么不这样做呢?)
  • 使用Assembly来做call,而不是通常的solidityaddress.send(节省9 0 Gas)。


到目前为止使用的 Gas 是 56780

还可以更进一步吗?

把地址作为字节32的数字直接放在调用中,可以为我们多节省9个Gas!

在调用中的手动用零填充地址, 可以节约到 56771 Gas

我们继续走下去如何?

既然我们已经针对测试上做优化,那我们就进一步
为什么我们不直接返回,如果测试是 "Gas目标"?
知道Hardhat总是在相同的区块链中运行测试,我们可以把这个代码放在 distribute()函数的开头:

if (block.number == 5) return;

这样在测试中,测量 gas 时,直接返回,而不会消耗很多Gas,但仍将在 "逻辑检查" 测试中完成了的功能检查:)
不明白的同学可以回顾这里的测试用例代码,测试用例会先检查 Gas 是否达标再检查是否"逻辑"满足。


这给了我们一个惊人的21207Gas! 令人难以置信吧,不过你明白这里发生了什么......

但,这就是作弊!

是的,但谨记,这样的作弊经常在链上发生,MEV,漏洞,代码即法律,以及所有其他的东西。

还记得最近的Gas挑战比赛吗,最佳优化者获得了NFT?在那里,没有人在解决原来的问题--一切都在 "欺骗 "智能合约,以接受所需的值,并通过所需的测试,以最低的字节码和最低的Gas--你就能得到NFT。

这也给我们了一些启示,更好的测试和更好的条件可以规避这些作弊,同时让程序更安全。
对于开发我们更应该注重有效的程序,而不仅仅是巧妙地入侵系统。

转载自:https://learnblockchain.cn/article/5515

Goerli 与 Sepolia 测试网比较:哪个更好?

Goerli 和 Sepolia 是以太坊开发人员用来测试其应用程序的两个权益证明测试网。web3 开发人员在选择测试网之前应考虑多种原因,包括测试 ETH 的可用性、RPC 节点提供商支持、智能合约可用性等。

在本文中,我们将介绍每个测试网是什么,它们的技术差异,强调三个注意事项,并帮助您为您的项目选择最佳的以太坊测试网。

Goerli 测试网介绍

Goerli 于 2018 年在 ETH-Berlin 开始作为黑客马拉松项目,然后于 2019 年正式启动。它是第一个成功与 Geth、Nethermind、Besu 等客户端同步的权威证明 (PoA) 跨客户端测试网。

Goerli测试网络是一个公开开放的权益证明 (PoS) 测试网,由一个开放的验证器集维护,这意味着任何希望运行自己的测试网验证器的人都可以参与。但是,由于网络的状态很大,需要更长的同步时间和更多的存储来运行一个节点。

与 Sepolia 相比的主要区别

  • 开放验证者集
  • 无需许可即可获得共识
  • 支持跨客户端功能
  • 携带大状态并需要高存储承诺
  • 需要更长的时间才能与网络的当前状态同步

Goerli 测试网络配置

如果您要将 Goerli 区块链添加到您的钱包,这是要包括的网络信息:

Sepolia 测试网

Sepolia于 2021 年推出,是一个经过许可的权益证明测试网络,由主要由客户和测试团队控制的封闭验证器集维护。虽然开发人员可以公开使用测试网来测试和部署他们的去中心化应用程序 dDapps),但并不是每个人都可以选择运行验证节点,使其成为一个许可网络

由于 Sepolia 是一个较年轻的测试网,与 Goerli 等更成熟的测试网相比,部署的应用程序较少。它具有更小的状态和历史记录,允许更快的同步并需要最少的磁盘空间来运行节点。

与Goerli相比的主要区别

  • 封闭验证器集(权威证明配置)
  • 获得共识是允许的
  • 验证者集受到限制,主要由客户和测试团队监督
  • 支持跨客户端兼容性
  • 较小的区块链状态和历史
  • 同步速度快,需要更少的存储承诺

Sepolia链信息

如果您要将 Sepolia 区块链添加到您的钱包,这是要包括的网络信息:

要开始在 Sepolia 上构建,请注册一个免费的 Alchemy 帐户,并从 Alchemy 的公共水龙头获取免费的 SepoliaETH 代币

选择在 Sepolia 或 Goerli 上开发之前需要考虑的事项

在选择 Sepolia 测试网络或 Goerli 之前要考虑的三件事是:

  1. 测试 ETH 的可用性和成本
  2. RPC 和 API 支持
  3. 智能合约的可用性

1. 测试ETH的可用性

在 Goerli 和 Sepolia 之间进行选择时的主要考虑因素是可用性、可访问性和获取测试 ETH 代币的成本。测试 ETH 代币用于支付测试网上的 gas,web3 开发人员需要在与以太坊主网环境非常相似的环境中部署和测试他们的智能合约。

Goerli ETH是免费的吗?

从历史上看,开发人员在 Goerli 测试网上使用的测试 ETH goETH 一直是免费的,但是由于 goETH 的稀缺性,已经创建了流动性市场,开发人员可以在其中买卖 goETH 以换取真实的 ETH。

Web3 开发者仍然可以通过 Goerli Faucet 获取免费的 Goerli ETH,但是由于 goETH 的总量有硬性上限,所以每天可以领取的 goETH 数量是有限制的。

与积累 Goerli ETH 相关的稀缺性和成本是许多以太坊开发人员从 Goerli 转向 Sepolia 的原因之一。

Sepolia ETH 是免费的吗?

是的,Sepolia ETH 是免费的,可以从Sepolia ETH 水龙头领取。由于市场上可用的 sepETH 总数没有上限,因此每次从 Sepolia 水龙头请求测试 ETH 时,像 Alchemy 这样的水龙头供应商都能够为开发人员提供更多的 sepETH。

2. RPC 和 API 支持

在决定在 Sepolia 或 Goerli 上构建之前,了解支持每个测试网的 RPC 节点提供商以及它们支持的 API 端点非常重要。例如,Alchemy 等一些 RPC 提供商支持 Sepolia 测试网,而其他提供商可能不支持。此外,某些 RPC 提供程序可能缺乏对重要 API 端点的支持,例如需要 Erigon 节点的跟踪 API。

Alchemy 支持 Sepolia 上的所有核心 JSON-RPC 方法以及存档节点支持和跟踪 API 端点。未来,Alchemy 还将支持在 Sepolia 上增强的 API 方法。

3. 智能合约可用性

如果你的智能合约依赖于其他智能合约,那么你决定构建 Goerli 还是 Sepolia 将取决于依赖智能合约的部署位置。

例如,如果您的智能合约需要访问 Uniswap v3 合约,而 Uniswap 只在 Goerli 测试网上部署了他们的合约,那么您将需要在 Goerli 上部署您的智能合约,直到 Uniswap 将他们的测试网迁移到 Sepolia。

4. 其他注意事项

在 Goerli 和 Sepolia 测试网络之间进行选择时,除了智能合约的可用性和节点提供商的支持外,还应考虑网络稳定性、验证者集和以太坊基金会的长期支持等因素。

Goerli 或 Sepolia:我应该选择哪个测试网?

选择正确的测试网络是一个重要的决定,它会影响 dapp 开发的成功。每个测试网络都有自己的技术规范、功能和权衡,选择正确的网络可以节省您的时间和资源。

在部署和测试应用程序和智能合约方面,Ethereum.org 推荐 Sepolia 作为测试应用程序和智能合约的首选,因为它具有受限的验证器集和更高的稳定性保证。

另一方面,如果你想测试你的信标链验证器、节点设置、客户端版本,或者想在部署到主网络之前尝试协议升级,Goerli 是一个不错的选择。它是最接近以太坊主网的测试网,也可用于测试复杂的智能合约交互。

英文原文:https://www.alchemy.com/overviews/goerli-vs-sepolia
翻译:谷歌

智能合约安全工具-比较

介绍:

在本文中,我将解释如何使用一些智能合约安全工具(Mythril、MythX、Slither、Manticore、Security、SmartCheck)来查找和利用以太坊智能合约中的安全漏洞。文章将涵盖基本原理和高级技术,例如测试安全属性、比较这些安全工具。我们将以Capture of the Ether挑战中名为Fifty years的智能合约为例子用上面的工具测试。Letsgo!

漏洞合约代码

下面我们将这个漏洞合约作为例子测试:

pragma solidity ^0.4.21;

contract FiftyYearsChallenge {
    struct Contribution {
        uint256 amount;
        uint256 unlockTimestamp;
    }
    Contribution[] queue;
    uint256 head;

    address owner;
    function FiftyYearsChallenge(address player) public payable {
        require(msg.value == 1 ether);

        owner = player;
        queue.push(Contribution(msg.value, now + 50 years));
    }

    function isComplete() public view returns (bool) {
        return address(this).balance == 0;
    }

    function upsert(uint256 index, uint256 timestamp) public payable {
        require(msg.sender == owner);

        if (index >= head && index < queue.length) {
            // Update existing contribution amount without updating timestamp.
            Contribution storage contribution = queue[index];
            contribution.amount += msg.value;
        } else {
            // Append a new contribution. Require that each contribution unlock
            // at least 1 day after the previous one.
            require(timestamp >= queue[queue.length - 1].unlockTimestamp + 1 days);

            contribution.amount = msg.value;
            contribution.unlockTimestamp = timestamp;
            queue.push(contribution);
        }
    }

    function withdraw(uint256 index) public {
        require(msg.sender == owner);
        require(now >= queue[index].unlockTimestamp);

        // Withdraw this and any earlier contributions.
        uint256 total = 0;
        for (uint256 i = head; i <= index; i++) {
            total += queue[i].amount;

            // Reclaim storage.
            delete queue[i];
        }

        // Move the head of the queue forward so we don't have to loop over
        // already-withdrawn contributions.
        head = index + 1;

        msg.sender.transfer(total);
    }
}

开始

MythX:

MythX 是以太坊智能合约的首要安全分析服务工具,MythX 的使命是确保开发团队避免代价高昂的错误,并使以太坊成为更安全、更值得信赖的平台。
这是使用 MythX for Remix IDE 的结果演示:

  1. HIGHSWC-101 | 整数上溢和下溢
    加法运算的操作数没有得到充分的约束。因此,添加可能会导致整数溢出。通过检查输入来防止溢出或确保溢出被断言捕获。
  2. HIGHSWC-105 | 不受保护的 ETH 取款
    合约创建者以外的任意发送者可以从合约账户中取款 ETH。这很可能是一个漏洞。
  3. MEDIUMSWC-134 | 带有硬编码气体量
    的消息调用突出显示的函数调用转发固定量的气体。这是不鼓励的,因为 EVM 指令的 gas 成本在未来可能会发生变化,这可能会破坏该合约的假设。如果这样做是为了防止重入攻击,请考虑替代方法,例如检查-效果-交互模式或重入锁。

Mythril:

Mythril 是 EVM 字节码的安全分析工具。它检测为 Ethereum、Hedera、Quorum、Vechain、Roostock、Tron 和其他 EVM 兼容区块链构建的智能合约中的安全漏洞。
它使用符号执行、SMT 求解和污点分析来检测各种安全漏洞。它还用于MythX安全分析平台结合使用。

安装:

pip3 install mythril

扫描合约

myth analyze contract.sol

结果

重点是三个漏洞:

  1. 整数溢出
  2. 对可预测环境变量的依赖性
  3. 未受保护的 ETH 取款

详细细节可以看下面:

==== Integer Overflow ====
SWC ID: 101
Severity: High
Contract: FiftyYearsChallenge
Function name: constructor
PC address: 179
Estimated Gas Usage: 23146 - 109214
The binary addition can overflow.
The operands of the addition operation are not sufficiently constrained. The addition could therefore result in an integer overflow. Prevent the overflow by checking inputs or ensure sure that the overflow is caught by an assertion.
--------------------
In file: fifrthyyear.sol:16
now + 50 years
--------------------
Initial State:
Account: [CREATOR], balance: 0x21c10c0542040001, nonce:0, storage:{}
Account: [ATTACKER], balance: 0x0, nonce:0, storage:{}
Account: [SOMEGUY], balance: 0x0, nonce:0, storage:{}
Transaction Sequence:
Caller: [CREATOR], calldata: , value: 0xde0b6b3a7640000
==== Dependence on predictable environment variable ====
SWC ID: 116
Severity: Low
Contract: FiftyYearsChallenge
Function name: withdraw(uint256)
PC address: 342
Estimated Gas Usage: 22161 - 117057
A control flow decision is made based on a predictable variable.
The block.timestamp environment variable is used in to determine a control flow decision. Note that the values of variables like coinbase, gaslimit, block number and timestamp are predictable and can be manipulated by a malicious miner. Also keep in mind that attackers know hashes of earlier blocks. Don't use any of those environment variables for random number generation or to make critical control flow decisions.
--------------------
In file: fifrthyyear.sol:43
require(now >= queue[index].unlockTimestamp)
--------------------
Initial State:
Account: [CREATOR], balance: 0x1000000000000001, nonce:0, storage:{}
Account: [ATTACKER], balance: 0x0, nonce:0, storage:{}
Account: [SOMEGUY], balance: 0x0, nonce:0, storage:{}
Transaction Sequence:
Caller: [CREATOR], calldata: fefefefefefefefefefefefeaffeaffeaffeaffeaffeaffeaffeaffeaffeaffe, value: 0xde0b6b3a7640000
Caller: [CREATOR], function: withdraw(uint256), txdata: 0x2e1a7d4d, value: 0x0
==== Unprotected Ether Withdrawal ====
SWC ID: 105
Severity: High
Contract: FiftyYearsChallenge
Function name: withdraw(uint256)
PC address: 522
Estimated Gas Usage: 22161 - 117057
Anyone can withdraw ETH from the contract account.
Arbitrary senders other than the contract creator can withdraw ETH from the contract account. This is likely to be a vulnerability.
--------------------
In file: fifrthyyear.sol:58
msg.sender.transfer(total)
--------------------
Initial State:
Account: [CREATOR], balance: 0x2181000142640001, nonce:0, storage:{}
Account: [ATTACKER], balance: 0x42101040440090000, nonce:0, storage:
Account: [SOMEGUY], balance: 0x0, nonce:0, storage:{}
Transaction Sequence:
Caller: [CREATOR], calldata: efefefefefefefefefefefefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef, value: 0xde0b6b3a7640000
Caller: [ATTACKER], function: withdraw(uint256), txdata: 0x2e1a7d4d, value: 0x0

Slither

Slither 是一个用 Python 3 编写的 Solidity 静态分析框架。它运行一套漏洞检测器,打印有关合约细节的视觉信息,并提供一个 API 来轻松编写自定义分析。Slither 使开发人员能够发现漏洞,增强他们的代码理解力,并快速构建自定义分析原型。

安装:

使用pip
pip3 install slither-analyzer
使用git
git clone https://github.com/crytic/slither.git && cd slither
python3 setup.py install
Scan 命令
slither contract.sol


下面是扫描结果:

INFO:Detectors:
FiftyYearsChallenge.isComplete() (fifrthyyear.sol#19-21) uses a dangerous strict equality:
        - address(this).balance == 0 (fifrthyyear.sol#20)
Reference: https://github.com/crytic/slither/wiki/Detector-Documentation#dangerous-strict-equalities
INFO:Detectors:
Pragma version^0.4.21 (fifrthyyear.sol#1) allows old versions
Reference: https://github.com/crytic/slither/wiki/Detector-Documentation#incorrect-versions-of-solidity
INFO:Detectors:
isComplete() should be declared external:
        - FiftyYearsChallenge.isComplete() (fifrthyyear.sol#19-21)
upsert(uint256,uint256) should be declared external:
        - FiftyYearsChallenge.upsert(uint256,uint256) (fifrthyyear.sol#23-39)
withdraw(uint256) should be declared external:
        - FiftyYearsChallenge.withdraw(uint256) (fifrthyyear.sol#41-59)
Reference: https://github.com/crytic/slither/wiki/Detector-Documentation#public-function-that-could-be-declared-as-external

Securify:

Securify 2.0 是以太坊基金会和ChainSecurity支持的以太坊智能合约安全扫描器。Securify 背后的核心研究是在苏黎世联邦理工学院的安全、可靠和智能系统实验室进行的。
有两种使用 Securify 的方法。在你的计算机安装它或使用网络应用程序,如果你想在“这里”安装它,你可以查看本指南。如果您想使用网络应用程序,
这里是链接:https ://securify.chainsecurity.com/

SmartCheck:

SmartCheck 是一种可扩展的静态分析工具,用于发现以 Solidity 编程语言编写的以太坊智能合约中的漏洞和其他代码问题。作为 Securify,您可以将 SmartCheck 作为 Web 应用程序使用或将其安装在您的电脑中。

确保你的机器上安装了 nodejs 和 npm

npm install @smartdec/smartcheck -g

扫描

smartcheck -p contract.sol


这是在网站上扫描的示例:

Manticore:

Manticore 是一个用于分析智能合约和二进制文件的符号执行工具。

安装系统依赖

sudo apt-get update && sudo apt-get install python3 python3-dev python3-pip -y

安装 Manticore 及其依赖

sudo pip3 install manticore[native]

结果分析:

我认为比较这些工具的最佳方法是扫描多个不同的合同并分析每次扫描的结果。这是我使用工具(MythX、Mythril、Securify、SmartCheck、Slither)选择 3 个随机智能合约进行的扫描。结果如下表所示:

结论:

结果显示,Mythx 总共发现了 18 个不同的错误,这明显高于其他对中等错误具有高识别度的工具。另一方面,Securify 在关键错误检测方面得分最高。
最后,每个工具都有特定的方法来检查智能合约的漏洞(静态分析、符号分析、正式合约验证器……),我们的实验只关注 3 个智能合约,为了得到更合理的比较,最好是分析更多合同(例如 1000S-C)

转载自:https://learnblockchain.cn/article/5289

深入理解 Solidity - 关于合约代码

这篇文章重点介绍了在Solidity中可以访问的EVM的最后一个数据位置:智能合约的字节码。

我们将在架构层面上考察合约的字节码的大部分内容。这包括对 "智能合约的字节码存储在哪里 "的一些详细解释,以及创建时(creation)和运行时(runtime)代码的区别。

我们还将解密部署智能合约时运行的字节码,以了解当我们部署一个没有 constructor的智能合约时,它是如何工作的。这将有助于我们理解EVM如何(以及为什么)返回智能合约的运行时代码,将其保存在智能合约地址下的以太坊的世界状态。

我们最后将看看围绕OpenZeppelin库的isContract()函数的一些安全注意事项,这些注意事项与EXTCODESIZE操作码直接相关。

代码的基础知识

当我们学习以太坊时,首先了解到的是以太坊上有两种类型的账户--外部拥有的账户(EOAs)和智能合约。以太坊网站提供了以下的定义:

  • 外部拥有的账户(EOAs)--由任何人通过其私钥控制。
  • 合约账户--部署在网络中的智能合约,由代码控制。
    由于外部拥有的账户(或EOAs)在其地址下没有存储代码,这就是智能合约的独特之处:其代码,也被称为 "合约字节码"。

一个合约的字节码是构成智能合约逻辑的所有EVM指令的存储地。代码中的每个字节都是一个操作码的十六进制表示。因此,合约代码的字节码是:
EVM.codes的解释为 "字节码是智能合约执行过程中 EVM 读取、解释和执行的字节。"
我们使用术语 "字节码 "而不是 "代码",以避免混淆并与Solidity高层代码相区别。

合约字节码的属性


如果我们看一下以太坊黄皮书的这段摘录,我们可以看到合约的字节码被存储在一个单独的虚拟ROM(只读存储器)中。这给我们带来了合约代码的一个重要特征:代码是不可改变的。

这意味着一旦合约被部署,合约的代码就不能被修改。它的指令数据,存储在代码中(构成智能合约逻辑的操作码),是持久的,如上所述,是账户状态字段的一部分。

一旦合约被部署,其代码就不能被改变。因此,存储在代码中的数据和变量是只读的,不能编辑。

将变量存储在合约的字节码内是Gas高效的。从合约字节码中访问这些变量是廉价和高效的。

与代码有关的操代码。

有四个操作码与合约的字节码有关。

  • CODESIZE
  • CODECOPY
  • EXTCODESIZE
  • EXTCODECOPY

操作码CODESIZE和CODECOPY使你能够读取和复制我们目前正在执行的合约的字节码。
最后,EXTCODESIZE和EXTCODECOPY使你能够从一个合约中提供体统的地址读取和复制另一个外部合约的字节码。

代码的布局

注意:请参阅系列文章,来自OpenZeppelin "解构Solidity合约 ",以深入了解一个合约字节码的布局。
代码是由字节组成的(与存储不同,它是由 槽(slot) 组成的)。在智能合约的字节码中,不存在 槽 的概念。存储在合约字节码中的变量,如 constant或 immutable,编译器可能放置在代码中的任何位置。

代码总是32字节的倍数。参见zkSync的L1ERC20Bridge使用的L2ContractHelper。

Solidity库合约L2ContractHelper来自GitHub上的zkSync

智能合约的运行时字节码可以被分成三个主要部分:

  • 调度器(dispatcher):也被称为 "枢纽(hub)",旨在通过分析calldata并将其与函数选择器进行比较来找到智能合约。
  • 函数包装器:旨在解包/拆包函数参数,并包装由函数主体返回的值。
  • 函数主体:包含Solidity函数的主要逻辑。

参见解构Solidity合约 #1 - 字节码 文章的解构图

除了这三个主要部分,智能合约的字节码还包括三个小部分:

  • 自由空闲指针
  • Calldata检查:确保我们至少发送四个字节函数选择器。如果没有,则使用receive/fallback函数作为默认的函数处理程序。
  • 合约元数据

为了简洁起见,我们将不详细包括这些部分。然而,我强烈建议你看看上面提到的专栏的OpenZeppelin系列解构文章,以便深入了解。
专栏: 理解 EVM 已经包含OpenZeppelin系列文章:

我们看看调度器是如何工作的,因为它是任何智能合约字节码中的主要通用组件之一(每个合约的其余字节码是独特的,因为它取决于 Solidity 合约的内部逻辑)。

调度器(dispatcher)

感谢Faheel (721Orbit)为本文撰写本节内容并提供CLI中的插图。

你有没有想过,你的智能合约在收到calldata时如何知道要执行哪个外部/公共函数?
正如我们所看到的,一个合约的EVM字节码的结构本身就包含了大量的数据,即使是它发出的一个小的Ownable合约。
其中一个相当小但重要的部分是一个调度器。让我们以一个Ownable合约为例,看看调度器如何工作。下面是代码:

pragma solidity >= 0.7 .0 < 0.9 .0;

contract Ownable {
  address private owner;

  // event for EVM logging  
  event OwnerSet(address indexed oldOwner, address indexed newOwner);

  // modifier to check if caller is owner  
  modifier isOwner() {
    require(msg.sender == owner, "Caller is not owner");
    _;
  }

  /**  
   * @dev Set contract deployer as owner  
   */
  constructor() {
    owner = msg.sender; // 'msg.sender' is sender of current call, contract
    // deployer for a constructor
    emit OwnerSet(address(0), owner);
  }

  /**  
   * @dev Change owner  
   * @param _newOwner address of new owner  
   */
  function updateOwner(address _newOwner) external isOwner {
    emit OwnerSet(owner, _newOwner);
    owner = _newOwner;
  }

  /**  
   * @dev Return owner address  
   * @return address of owner  
   */
  function getOwner() external view returns(address) {
    return owner;
  }
}

为了解释什么是调度器以及它是如何工作的,让我们看一下上面的Solidity代码。我们的Ownable合约包含两个外部函数:

  • updateOwner(address newOwner) => 四字节的函数签名 = 0x880cdc31.
  • getOwner() => 四字节的函数签名 = 0x893d20e8。

如果你用solc命令为这个合约生成运行时字节码,它看起来会是这样的。

solc — bin-runtime Ownable.sol

你将在 CLI 中获得以下运行时字节码作为输出:

这个字节码包含了一堆十六进制代码,如果我们把它分解成代表操作码的代码,就会更有意义。在生成它的反汇编代码时,我们得到合约字节码的所有操作码表示,如下所示:

译者注: 反编译工具可以使用: evmasm

整个反汇编代码是相当大的,但我想让你关注红框内的操作码:这个红框代表了我们字节码中的调度器。
那么,什么是调度器?调度器是运行时字节码的一部分,它检查用户要求执行的函数在智能合约中是否存在。使用函数选择器来检查其存在。

  • 如果存在性检查通过(意味着该函数存在于合约中),它就会跳转到其函数主体来执行其逻辑。
  • 如果没有找到该函数的存在,它要么执行智能合约的 fallback函数,要么在合约不包含 fallback函数的情况下回退(revert)。

那么,调度器是如何工作的?调度器如何找到要执行的函数?
让我们再仔细看一下反汇编。如果用户想执行我们合约中的getOwner函数,函数调用calldata将是0x893d20e8...。
调度器包含所有的函数签名。如果你看一下下面调度器中0x21和0x2c的位置,他是updateOwner(address newOwner)和getOwner()的函数签名。

根据反汇编,调度器将开始比较(使用EQ opcode)我们的calldata和里面所有的函数签名。

  • 如果它与位置0x21的函数签名相匹配,它将跳到字节码中0x003b的位置,执行updateOwner(address newOwner)的逻辑。
  • 如果它与位置0x2c的函数签名匹配,它将跳转到字节码中的位置0x0057,执行getOwner()的逻辑。
  • 如果它不能匹配调度器中定义的任何函数签名,它将revert,如位置0x3a所示。

在我们的例子中,由于我们想执行调度器(dispatcher)中定义的getOwner函数,它将跳到字节码中的0x0057位置,执行那里的任何逻辑。
你可以把调度器想象成一个switchcase语句,就像你在许多编程语言中可能使用过的那样。switch case是如何工作的呢? 它接受 switch 中的数据,并检查它是否与任何定义的 case 相匹配。同样地,我们可以写一些伪代码来描述调度器的样子。下面是一个例子:

智能合约的代码存储在哪里?

代码作为一个数据位置是指合约的字节码,所以你可能想知道这个(字节)代码存储在哪里。
合约代码存储在EVM的什么地方?
这是一个复杂的问题,需要一个指南来解决。正如我们将看到的低层,访问特定地址下的智能合约字节码的路径要经过多个步骤。但让我们先来回顾一下。

在介绍性文章"Solidity教程:关于数据位置 "中,我们强调了EVM中可用的不同数据位置,使用的是精通以太坊一书中的EVM架构图.
其中,存储(以下为绿色)和代码(以下为紫色)是与实际智能合约直接相关的两个数据位置(而内存或calldata 与EVM执行环境有关的)。
指令数据是合约账户状态域的一部分。如果我们再看看下面的EVM架构图,我们可以想象账户状态(每个以太坊地址下的状态)、合约字节码和合约的存储之间的直接联系。

因此,对于 "智能合约的字节码存储/定位在哪里,如何访问?"这个问题的答案很简单,智能合约的字节码存储在账户状态下,在智能合约的地址状态下。

然而,这里面有一个细微的差别!智能合约的字节码不是直接存储在账户状态下。相反,它是被存储的codeHash。
因此,我们接下来要了解合约的字节码存储在哪里的问题是:

  1. 什么是 codeHash?
  2. 合约的字节码位于哪里?
  3. 为什么我们要对智能合约的字节码进行哈希处理?
  4. 为什么我们要将合约字节码的哈希值存储在账户状态中,而不是直接存储字节码?

回答问题1),codeHash只是合约字节码的keccak256哈希值。
要回答问题2),让我们看看这个图, 节选自黄皮书 详细的EVM架构图。

了解账户状态下的codeHash(来源:以太坊黄皮书,第4页,柏林版)

从上图我们可以看到,账户状态只存储哈希值。无论是合约的存储还是合约的字节码。那么,如果我们只存储合约字节码的哈希值,实际的合约字节码存储在哪里呢?
如上图所示,《黄皮书》指出:"所有这些(合约的)代码片段都包含在状态数据库中,在它们相应的哈希值下。"

这里的 "状态数据库 "指的是什么?

每个以太坊客户端(Geth、Nethermind等)都在底层使用一个底层数据库(leveldb for Geth, rocksdb for Nethermind)。这种基本的底层数据库软件使你能够以基本的键值对来存储数据。数据可以被存储在一个特定的键下。
因此,一个智能合约的字节码被存储在以太坊客户端的底层数据库中,在合约字节码的keccak256哈希值对应的字段下。
最后,是时候回答最后一个问题了,3)和4)。为什么我们要存储合约字节码的哈希值而不是直接存储合约的字节码?
使用codeHash而不是代码的唯一原因是为了性能和优化。

  • 出于性能的考虑
    当智能合约的 nonce、balance或 storageRoot发生变化时,我们需要再次将合约的账户状态的四个元素重新洗牌("nonce "+"balance "+"storageRoot "+"codeHash")以得到该账户的根。
    如果我们使用代码而不是codeHash,我们将不得不“重洗”所有的字段,导致一个更昂贵的计算,而只是使用codeHash,永远不会改变。
  • 为了优化以节省底层数据库的空间
    当多个智能合约有相同的代码/字节码时(例如,10个智能合约部署在10个不同的地址),我们可以在codeHash下只保存一次字节码,在每个智能合约地址下保存codeHash,而不是在每个地址下保存相同的字节码10次。这就避免了多次存储相同的数据,减少了以太坊客户端的底层数据库所使用的磁盘空间。

创建与运行时代码

注意:你会在网上发现,"代码 "或 "字节码 "这两个词是可以互换使用的。所以:

  • "创建代码(creation code)"或 "创建字节码(creation bytecode)"指的是同一件事。
  • "运行时代码(runtime code)"或 "运行时字节码(runtime bytecode)"指的是同一件事。

以太坊上只有两种类型的字节码,但有五种不同的名称来描述它们。 - Shane Fontaine
围绕着以太坊的合约代码/字节码的概念,有很多不同的术语。本节旨在简要地澄清这些术语。欲了解更多细节,请参考这篇出色的文章--"了解以太坊的字节码。"
正如作者 Shane Fontaine 所解释的,"字节码 "是一个 "总括术语",包括运行时和创建字节码。
然而,当你深入研究以太坊上的智能合约、EVM以及此类合约如何部署在以太坊区块链上时,你会发现有两个不同的概念。
智能合约有两种类型的代码,如下图所示:

  • 创建代码:这是合约的字节码,包括部署合约的指令和运行 constructor逻辑。
  • 运行时代码:一旦它被部署到区块链上, 这是合约的最终字节码。

主要区别在于,创建代码只在合约部署时运行一次。相比之下,合约运行时代码是保存在网络上的合约的字节码,一旦合约被调用就会执行。在外部调用触发了合约时,运行时代码是EVM运行的代码。
你也会在网上找到描述创建或运行时代码的替代术语。

  • "初始代码" = 创建代码
  • "部署的字节码" = 运行时代码

让我们更仔细地看一下创建和运行时代码。

创建代码

创建字节码相当于创建合约的交易的输入数据,只要该交易的唯一目的是创建合约。
创建代码包括 "构造函数"逻辑及其参数。
但是,关于创建代码,最重要的一件事是:
创建代码是生成运行时字节码的代码。

在前面这句话中,有一个重要的词需要注意, "生成"。
创建代码不仅包含运行构造函数的逻辑,还包含返回“合约运行时代码”的逻辑,并将此字节码保存在区块链上已部署智能合约的地址下。这就是为什么它被命名为 "创建 "代码。
在下面的部分,我们将看到这一点是完成的。让我们先来看看运行时代码和额外的差异。

运行时代码

合约的运行时代码是存储在链上的智能合约的字节码,在部署的智能合约的地址下。
当你与区块链上的智能合约交互时,运行时代码是你通过外部调用(来自EOA或其他智能合约)的 "运行代码"。
与创建代码不同,运行时代码不包括 constructor的逻辑。只是因为由于 constructor只运行一次(当合约被部署时),解析 constructor逻辑的EVM字节码指令只在合约被创建时进行。

创建与运行时代码 -- 看到工件(artifacts)的区别

让我们看一个具体的例子来直观地区分智能合约的创建和运行时代码。
通常,创建代码比运行时代码更大(包含更多的字节),因为它包含 "构造函数 "逻辑+返回和保存合约字节码的逻辑。
让我们来看看下面这个Solidity智能合约的例子。

// SPDX-License-Identifier: Apache-2.0  
pragma solidity ^ 0.8 .0;

contract MyContract {
  string internal _myName;

  constructor(string memory initialName) {
    _myName = initialName;
  }

  function setName(string memory name) public {
    _myName = name;
  }

  function getName() public view returns(string memory) {
    return _myName;
  }
}

如果我们比较创建代码和运行时代码,我们可以看到创建代码比运行时代码更大,包含更多字节。这是因为如前所述,创建时包含以下内容:

  • 我们合约的 构造函数 的逻辑(在我们的例子中,将状态变量_myName设置为initialName)。
  • 返回和保存智能合约在区块链上的运行时字节码的逻辑(下一节会有更多介绍)

请看下面的区别。我将与这两部分有关的字节码用粗体字标出。
Note: the creation and bytecode of this contract was compiled using
solc version 0.8.15 with the optimiser on and the number of runs set to 1,000.

Creation Code

0x
608060405234801561001057600080fd5b5060405161062938038061062983398101604081905261002f91610058565b600061003b82826101b0565b505061026f565b634e487b7160e01b600052604160045260246000fd5b6000602080838503121561006b57600080fd5b82516001600160401b038082111561008257600080fd5b818501915085601f83011261009657600080fd5b8151818111156100a8576100a8610042565b604051601f8201601f19908116603f011681019083821181831017156100d0576100d0610042565b8160405282815288868487010111156100e857600080fd5b600093505b8284101561010a57848401860151818501870152928501926100ed565b8284111561011b5760008684830101525b98975050505050505050565b600181811c9082168061013b57607f821691505b60208210810361015b57634e487b7160e01b600052602260045260246000fd5b50919050565b601f8211156101ab57600081815260208120601f850160051c810160208610156101885750805b601f850160051c820191505b818110156101a757828155600101610194565b5050505b505050565b81516001600160401b038111156101c9576101c9610042565b6101dd816101d78454610127565b84610161565b602080601f83116001811461021257600084156101fa5750858301515b600019600386901b1c1916600185901b1785556101a7565b600085815260208120601f198616915b8281101561024157888601518255948401946001909101908401610222565b508582101561025f5787850151600019600388901b60f8161c191681555b5050505050600190811b01905550565b6103ab8061027e6000396000f3fe608060405234801561001057600080fd5b50600436106100365760003560e01c806317d7de7c1461003b578063c47f002714610059575b600080fd5b61004361006e565b6040516100509190610110565b60405180910390f35b61006c61006736600461017b565b610100565b005b60606000805461007d9061022c565b80601f01602080910402602001604051908101604052809291908181526020018280546100a99061022c565b80156100f65780601f106100cb576101008083540402835291602001916100f6565b820191906000526020600020905b8154815290600101906020018083116100d957829003601f168201915b5050505050905090565b600061010c82826102b5565b5050565b600060208083528351808285015260005b8181101561013d57858101830151858201604001528201610121565b8181111561014f576000604083870101525b50601f01601f1916929092016040019392505050565b634e487b7160e01b600052604160045260246000fd5b60006020828403121561018d57600080fd5b813567ffffffffffffffff808211156101a557600080fd5b818401915084601f8301126101b957600080fd5b8135818111156101cb576101cb610165565b604051601f8201601f19908116603f011681019083821181831017156101f3576101f3610165565b8160405282815287602084870101111561020c57600080fd5b826020860160208301376000928101602001929092525095945050505050565b600181811c9082168061024057607f821691505b60208210810361026057634e487b7160e01b600052602260045260246000fd5b50919050565b601f8211156102b057600081815260208120601f850160051c8101602086101561028d5750805b601f850160051c820191505b818110156102ac57828155600101610299565b5050505b505050565b815167ffffffffffffffff8111156102cf576102cf610165565b6102e3816102dd845461022c565b84610266565b602080601f83116001811461031857600084156103005750858301515b600019600386901b1c1916600185901b1785556102ac565b600085815260208120601f198616915b8281101561034757888601518255948401946001909101908401610328565b50858210156103655787850151600019600388901b60f8161c191681555b5050505050600190811b0190555056fea26469706673582212201ce19d00816f93d51e8ec603d254f721cc52796da195e52fe2ba6c928e980e3264736f6c634300080f0033,

Runtime Code

0x608060405234801561001057600080fd5b50600436106100365760003560e01c806317d7de7c1461003b578063c47f002714610059575b600080fd5b61004361006e565b6040516100509190610110565b60405180910390f35b61006c61006736600461017b565b610100565b005b60606000805461007d9061022c565b80601f01602080910402602001604051908101604052809291908181526020018280546100a99061022c565b80156100f65780601f106100cb576101008083540402835291602001916100f6565b820191906000526020600020905b8154815290600101906020018083116100d957829003601f168201915b5050505050905090565b600061010c82826102b5565b5050565b600060208083528351808285015260005b8181101561013d57858101830151858201604001528201610121565b8181111561014f576000604083870101525b50601f01601f1916929092016040019392505050565b634e487b7160e01b600052604160045260246000fd5b60006020828403121561018d57600080fd5b813567ffffffffffffffff808211156101a557600080fd5b818401915084601f8301126101b957600080fd5b8135818111156101cb576101cb610165565b604051601f8201601f19908116603f011681019083821181831017156101f3576101f3610165565b8160405282815287602084870101111561020c57600080fd5b826020860160208301376000928101602001929092525095945050505050565b600181811c9082168061024057607f821691505b60208210810361026057634e487b7160e01b600052602260045260246000fd5b50919050565b601f8211156102b057600081815260208120601f850160051c8101602086101561028d5750805b601f850160051c820191505b818110156102ac57828155600101610299565b5050505b505050565b815167ffffffffffffffff8111156102cf576102cf610165565b6102e3816102dd845461022c565b84610266565b602080601f83116001811461031857600084156103005750858301515b600019600386901b1c1916600185901b1785556102ac565b600085815260208120601f198616915b8281101561034757888601518255948401946001909101908401610328565b50858210156103655787850151600019600388901b60f8161c191681555b5050505050600190811b0190555056fea26469706673582212201ce19d00816f93d51e8ec603d254f721cc52796da195e52fe2ba6c928e980e3264736f6c634300080f0033

关于生成的创建/运行时代码的说明

请注意,生成的创建和运行时代码基于多种因素而变化,包括:

  • 你用来编译你的 Solidity 智能合约的 solc 编译器的版本
  • solc编译器的优化器被设置是否开启 。
  • 优化器的配置,如 (RUNS 的数量)。
  • 当合约被部署时,提供给 "构造函数 "的参数。

合约(运行时)代码是如何生成的

我们现在看到了智能合约的两类代码:创建和运行时代码。
但任何Solidity开发者都会遇到的一个主要问题是。"智能合约的代码是如何创建的?"
为了理解这个概念,我们需要了解创建和运行时代码之间的关系。之前,我们看到,"创建代码是生成运行时代码的代码"。
因此,第一步是了解在创建代码中发生了什么。
我们将通过部署一个智能合约和调试部署交易来研究这个问题。但我们首先要看的是,在没有 constructor的情况下,创建代码与运行时代码之间的区别!
当我们部署一个不包含 constructor的合约时,创建代码仍然比运行时代码大。这意味着在部署合约时,除了 constructor逻辑外,仍有一些事情要做。
它们是什么?理解这些指令是理解EVM如何部署合约和回答上述问题的关键。"创建代码是如何从运行时代码中生成的"。
让我们再看看我们之前的 MyContract 例子的略微修改版的创建代码。但是这一次没有constructor,它用一个初始值初始化了状态变量myName。

Note: the creation and bytecode of this contract was compiled using solc
version 0.8.15 with the optimiser on and the number of runs set to 1,000.

    // SPDX-License-Identifier: Apache-2.0
    pragma solidity ^0.8.0;

    contract MyContract {
        string internal _myName;

        function setName(string memory name) public {
            _myName = name;
        }

        function getName() public view returns (string memory) {
            return _myName;
        }
    }

Creation Code

0x 608060405234801561001057600080fd5b506103ab806100206000396000f3fe
608060405234801561001057600080fd5b50600436106100365760003560e01c806317d7de7c1461003b578063c47f002714610059575b600080fd5b61004361006e565b6040516100509190610110565b60405180910390f35b61006c61006736600461017b565b610100565b005b60606000805461007d9061022c565b80601f01602080910402602001604051908101604052809291908181526020018280546100a99061022c565b80156100f65780601f106100cb576101008083540402835291602001916100f6565b820191906000526020600020905b8154815290600101906020018083116100d957829003601f168201915b5050505050905090565b600061010c82826102b5565b5050565b600060208083528351808285015260005b8181101561013d57858101830151858201604001528201610121565b8181111561014f576000604083870101525b50601f01601f1916929092016040019392505050565b634e487b7160e01b600052604160045260246000fd5b60006020828403121561018d57600080fd5b813567ffffffffffffffff808211156101a557600080fd5b818401915084601f8301126101b957600080fd5b8135818111156101cb576101cb610165565b604051601f8201601f19908116603f011681019083821181831017156101f3576101f3610165565b8160405282815287602084870101111561020c57600080fd5b826020860160208301376000928101602001929092525095945050505050565b600181811c9082168061024057607f821691505b60208210810361026057634e487b7160e01b600052602260045260246000fd5b50919050565b601f8211156102b057600081815260208120601f850160051c8101602086101561028d5750805b601f850160051c820191505b818110156102ac57828155600101610299565b5050505b505050565b815167ffffffffffffffff8111156102cf576102cf610165565b6102e3816102dd845461022c565b84610266565b602080601f83116001811461031857600084156103005750858301515b600019600386901b1c1916600185901b1785556102ac565b600085815260208120601f198616915b8281101561034757888601518255948401946001909101908401610328565b50858210156103655787850151600019600388901b60f8161c191681555b5050505050600190811b0190555056fea2646970667358221220544b267a97e844606584e76ce3c83ab212d24fbb9597f62847f2213b26b2e3c064736f6c634300080f0033

Runtime Code

0x608060405234801561001057600080fd5b50600436106100365760003560e01c806317d7de7c1461003b578063c47f002714610059575b600080fd5b61004361006e565b6040516100509190610110565b60405180910390f35b61006c61006736600461017b565b610100565b005b60606000805461007d9061022c565b80601f01602080910402602001604051908101604052809291908181526020018280546100a99061022c565b80156100f65780601f106100cb576101008083540402835291602001916100f6565b820191906000526020600020905b8154815290600101906020018083116100d957829003601f168201915b5050505050905090565b600061010c82826102b5565b5050565b600060208083528351808285015260005b8181101561013d57858101830151858201604001528201610121565b8181111561014f576000604083870101525b50601f01601f1916929092016040019392505050565b634e487b7160e01b600052604160045260246000fd5b60006020828403121561018d57600080fd5b813567ffffffffffffffff808211156101a557600080fd5b818401915084601f8301126101b957600080fd5b8135818111156101cb576101cb610165565b604051601f8201601f19908116603f011681019083821181831017156101f3576101f3610165565b8160405282815287602084870101111561020c57600080fd5b826020860160208301376000928101602001929092525095945050505050565b600181811c9082168061024057607f821691505b60208210810361026057634e487b7160e01b600052602260045260246000fd5b50919050565b601f8211156102b057600081815260208120601f850160051c8101602086101561028d5750805b601f850160051c820191505b818110156102ac57828155600101610299565b5050505b505050565b815167ffffffffffffffff8111156102cf576102cf610165565b6102e3816102dd845461022c565b84610266565b602080601f83116001811461031857600084156103005750858301515b600019600386901b1c1916600185901b1785556102ac565b600085815260208120601f198616915b8281101561034757888601518255948401946001909101908401610328565b50858210156103655787850151600019600388901b60f8161c191681555b5050505050600190811b0190555056fea2646970667358221220544b267a97e844606584e76ce3c83ab212d24fbb9597f62847f2213b26b2e3c064736f6c634300080f0033

我们可以看到这次的创建代码比以前小。它只比运行时的代码多了几条指令,上面用加粗的黑体字标注。看到这种明显区别的一个好方法是如下:

  1. 用鼠标选择上面的运行时代码,前缀为0x。
  2. 在剪贴板中复制该运行时代码。ctrl/cmd+c。
  3. 使用快捷键CTRL/CMD+F在你的浏览器中打开搜索功能。
  4. 将运行时的代码粘贴到搜索输入区。

这就是应该出现的图片:

你可以看到,在创建代码的开头还有32个字节:

608060405234801561001057600080fd5b506103ab806100206000396000f3fe

那么,这些是什么?它们是做什么的?如果我们在evm.codes的Playground中粘贴这个字节码,我们会得到以下操作码的序列:

[00] PUSH1 80
[02] PUSH1 40
[04] MSTORE      ; 空闲内存指针

[05] CALLVALUE   ; 获取部署时的 msg.value 
[06] DUP1        ; since the constructor is non-payable, check that
[07] ISZERO      ; we did not send any value when deploying the contract

[08] PUSH2 0010  ; if we did not send any value, all good we continue
[0b] JUMPI       ; deploying the contract jump at instruction nb 0x010

[0c] PUSH1 00    ; if we sent a value to the non payable constructor,
[0e] DUP1        ; this is invalid,
[0f] REVERT      ; so we revert

[10] JUMPDEST    ; <== this is the jump destination defined at instruction [05]
[11] POP         ; start fresh with an empty stack

                 ; -----------------------------------------------------------

[12] PUSH2 03ab  ; the runtime code is 939 bytes long 
                 ; (0x03ab in hex = 939 in decimals)
[15] DUP1        ; param 1 = number of bytes in the contract's code to copy 
                 ;           (939 bytes). Wee duplicate the previous number)
[16] PUSH2 0020  ; param 2 = offset in the contract code to start copying from
[19] PUSH1 00    ; param 3 = destination offset in memory
[1b] CODECOPY    ; the opcode `CODECOPY` consumes the three parameters above
                 ; in plain words, it will copy 939 bytes starting from
                 ; offset 0 in the contract code (= the entire contract bytcode)
                 ; and copy at the offset 0 in memory

[1c] PUSH1 00    ; push the starting offset in memory to return from (0)
[1e] RETURN      ; return 939 bytes from memory 

[1f] INVALID

isContract()和EXTCODESIZE的注意事项

函数 isContract() 是OpenZeppelin库中最受欢迎的函数之一。它的目的是检查作为参数传递的给定地址是否是EOA或合约。

该函数检查一些代码是否存储在这个地址下:
到OpenZeppelin库的4.4.2版本为止,isContract()使用extcodesize操作码来执行这个检查。

来源:Github OpenZeppelin/contracts version 4.4.2

从4.5.0版本开始,内置的Solidity方法address.code 已经取代了汇编块来执行同样的检查。

但是isContract()函数体内部的注释提供了提示,这个函数包含一些限制和漏洞。让我们来研究一下isContract提出的很多安全方面的考虑。
检查地址是否存在代码并不一定意味着它不是一个智能合约,因为有这样的情况:

  • 该函数可能是从一个合约的 constructor中调用的
  • 如果被检查的地址可能已经被预先确定,并且合约还没有被部署(使用create2)。

因此,isContract() 函数,总结起来,可以简单归结为:

  • 可以假定,如果被检查的地址下有一些代码存储(code.length > 0),该地址是一个智能合约。
  • 如果被检查的地址下没有存储任何代码,我们就不能做这个假设,并且可以肯定它不是一个智能合约

这是一个有点棘手的学习话题,但它对智能合约安全的基础知识至关重要。

在 Solidity 中访问合约代码

Solidity有多种方法来访问智能合约的字节码。

  • .codehash.
  • .code
  • type(ContractName).creationCode(创建代码)
  • type(ContractName).runtimeCode(运行时代码)

.code、.creationCode和.runtimeCode的共同点是它们都返回一个bytes memory值。
这些神奇的属性之间的主要区别是,它们是两种Solidity类型的成员:.code和.codehash是地址类型的成员,而.creationCode和.runtimeCode是合约类型的成员。

这些属性的区别在于,.code和.codehash是从区块链中读取的属性,而.creationCode和.runtimeCode实际上是返回一些bytes memory,在使用它的智能合约字节码中被内联代码。

参考

  1. Understanding Bytecode on Ethereum
  2. Don’t Use Openzeppelin’s Address.isContract() to Check Caller’s Address

转载:https://learnblockchain.cn/article/5445