本文我们来尝试进行 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
版权属于:区块链中文技术社区 / 转载原创者
本文链接:https://bcskill.com/index.php/archives/1735.html
相关技术文章仅限于相关区块链底层技术研究,禁止用于非法用途,后果自负!本站严格遵守一切相关法律政策!