本文深入探讨了Solidity中unchecked
块的使用,旨在在保证安全性的前提下进行Gas优化。
Photo by Emmanuel Edward on Unsplash
上周,我盯着我的以太坊 gas 成本,畏缩了。又一次部署,又一笔小财富花掉了。那时我决定深入研究 Solidity 中一个更加细致的特性:`unchecked` 块。
你看,虽然大多数 Solidity 开发者都知道 `unchecked` math(数学运算)是为了优化 gas,但很少有人花时间围绕它构建适当的安全机制。今天,我想分享一段旅程,这段旅程引导我创建了`EnhancedSafeMathTester` 合约 —— 一个演示我们如何兼得(gas 节省)和安全。
## Gas 问题
对于 Solidity 开发的新手来说,每个操作都需要消耗 gas。自从 Solidity 0.8.0 以来,算术运算会自动检查溢出和下溢 —— 这是一个很棒的安全特性,但它也带来了 gas 成本。
考虑一个简单的循环:
```solidity
for (uint i = 0; i < array.length; i++) {
// 做一些事情
}
\`i\` 的每次递增都包括一个我们很少需要的溢出检查。在一个大型循环中,这些检查会迅速累积。
**\## Unchecked 解决方案(及其问题)**
输入 \`unchecked\` 块:
for (uint i = 0; i < array.length;) {
// 做一些事情
unchecked { i++; }
}
这节省了 gas,但移除了一个重要的安全网。如果使用不当,\`unchecked\` 可能会导致漏洞,比如困扰早期智能合约的臭名昭著的整数溢出攻击。
我想开发在各种场景中安全使用 \`unchecked\` 的模式。这就是引导我构建 \`EnhancedSafeMathTester\` 的原因。
**\## 构建 EnhancedSafeMathTester**
我的方法是有条不紊的。首先,我需要一个变量来演示溢出问题 —— 一个初始化为接近最大值的\`uint8\` 似乎是完美的:
uint8 public bigNumber = 225;
最大值为 255,这给了我们足够的操作空间,但又足够接近边缘,可以演示潜在的问题。
然后,我实现了处理算术的多种方法,每种方法都有不同的安全/gas 权衡:
**\### 1. 故意不安全的方法**
function unsafeAdd() public whenNotStopped {
uint8 oldValue = bigNumber;
unchecked {
bigNumber += 1;
}
emit ValueChanged(oldValue, bigNumber, "unsafeAdd");
}
这个函数演示了不应该做什么 —— 使用 \`unchecked\` 而不使用任何安全机制。我把它作为一个警告,并用于比较。
**\### 2. 预操作验证**
function safeAdd(uint8 x) public whenNotStopped {
// 在执行 unchecked 操作之前进行手动检查
require(bigNumber <= 255 - x, "加法会导致溢出");
uint8 oldValue = bigNumber;
unchecked {
bigNumber += x;
}
emit ValueChanged(oldValue, bigNumber, "safeAdd");
}
在这里,我们在执行操作之前手动验证该操作是否会溢出。这给了我们 gas 节省,同时保持了安全。
**\### 3. 有界限的操作**
function boundedIncrement() public whenNotStopped {
uint8 oldValue = bigNumber;
unchecked {
// 确保值保持在界限内
if (bigNumber < 255) {
bigNumber += 1;
}
}
emit ValueChanged(oldValue, bigNumber, "boundedIncrement");
}
这种方法在\`unchecked\` 块中使用一个条件来防止溢出。它对于递增计数器特别有用。
**\### 4. 数学边界检查**
function doubleIfSafe() public whenNotStopped {
// 确保翻倍不会溢出
require(bigNumber <= 127, "翻倍会导致溢出");
uint8 oldValue = bigNumber;
unchecked {
bigNumber *= 2;
}
emit ValueChanged(oldValue, bigNumber, "doubleIfSafe");
}
对于乘法,我们需要不同的检查。这个函数确保该值可以安全地翻倍。
**\## 添加安全机制**
算术安全是不够的。我还实现了几个额外的安全模式:
**\### 1. 紧急断路器**
bool public emergencyStop = false;
modifier whenNotStopped() {
require(!emergencyStop, "合约已暂停");
_;
}
function toggleEmergencyStop() public onlyOwner {
emergencyStop = !emergencyStop;
emit EmergencyToggled(emergencyStop);
}
如果出现问题,这允许暂停功能 —— 对于任何生产合约来说,这都是一个关键的安全机制。
**\### 2. 访问控制**
address public owner;
modifier onlyOwner() {
require(msg.sender == owner, "未授权");
_;
}
将敏感功能限制在授权用户范围内是一个基本但必不可少的安全模式。
**\### 3. 事件监控**
event ValueChanged(uint8 oldValue, uint8 newValue, string operation);
为所有状态更改发出事件,可以更容易地监控合约并检测异常行为。
**\## 真实世界的应用**
虽然我的 \`EnhancedSafeMathTester\` 是一个演示合约,但这些模式适用于许多真实世界的场景:
**\### DeFi 协议**
在 DeFi 中,由于高交易量,gas 优化至关重要。使用安全的 \`unchecked\` math 可以显著降低用户的成本,同时保持安全性。
**\### NFT 铸造**
当铸造数千个 NFT 时,计数器递增的 gas 节省会迅速累积。有界递增模式在这里非常有效。
**\### DAO 投票系统**
投票计数通常涉及大量的迭代。使用经过适当验证的 unchecked math 可以使投票机制更具有 gas 效率。
**\## 测试实现**
在使用这些模式部署任何合约之前,我总是建议:
1\. 每个函数的单元测试,测试正常操作和边缘情况
2\. 模糊测试以捕获意外的输入组合
3\. 如果可能,进行形式化验证
4\. Gas 分析以确认优化正在工作
**\## 经验教训**
通过构建这个合约,我学到了几个重要的教训:
1\. **\*\*安全第一,gas 优化第二\*\*** —— 在核心功能安全之前,永远不要优化
2\. **\*\*执行前验证\*\*** —— 预先检查条件使 \`unchecked\` 块可以安全使用
3\. **\*\*分层安全机制\*\*** —— 结合多种模式以实现强大的保护
4\. **\*\*广泛记录\*\*** —— 正如你可以从我的合约注释中看到的那样,清晰的文档有助于其他人理解你的安全方法
**\## 结论**
Gas 优化不必以牺牲安全性为代价。通过仔细的计划和适当的模式,我们可以安全地使用像 \`unchecked\` math 块这样的功能。
\`EnhancedSafeMathTester\` 仅仅演示了这种平衡的几种方法。我希望这些模式在你的项目中被证明是有用的,在节省 gas 的同时保持合约的安全。
请记住 —— 在区块链开发中,你不仅仅是在编写代码;你正在编写一旦部署就无法修补的不可变的金融逻辑。花时间把它做好。
**你对 Solidity 中的 gas 优化有什么经验?你是否发现了安全使用 \`unchecked\` math 的其他模式?在下面的评论中分享你的想法!**
— -
**关于作者:一位充满激情的智能合约开发者,专注于为 DeFi 和 Web3 应用程序提供安全、gas 高效的实现。**
>- 原文链接: [coinsbench.com/solidity-...](https://coinsbench.com/solidity-unchecked-math-beyond-the-basics-making-gas-optimisation-safe-513bf4dedd61)
>- 登链社区 AI 助手,为大家转译优秀英文文章,如有翻译不通的地方,还请包涵~
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!