本文我们来尝试进行 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