避免存储值从零变为非零
分析
初始化存储变量是合约中最昂贵的操作之一。在 Solidity 智能合约中,将存储变量从零(默认值)更新为非零值是一项代价高昂的操作。这种“零到非零”的存储写入通常会消耗 22,100 gas,其中包括:
-
20,000 gas:用于将存储槽从零设置为非零值(SSTORE 操作)。
-
2,100 gas:用于首次访问该存储槽时的“冷访问”成本。
相比之下,其他类型的存储写入操作成本较低:
-
非零到非零:5,000 gas。
-
非零到零:5,000 gas,并可能获得最多 %25的退款。
这种高昂的成本主要是因为将存储槽从零更新为非零值会增加以太坊区块链的整体状态大小,要求所有节点存储更多数据,从而增加了网络的存储负担。为了优化 gas 使用,建议尽量避免不必要的“零到非零”存储写入。例如,OpenZeppelin 的 ReentrancyGuard 使用 1 和 2 来表示状态,而不是 0 和 1,以避免从零开始的写入操作。
这个模式的核心原则是:对于一个频繁在几个状态之间切换的内部状态变量,如果它的“默认”或“闲置”状态不需要是 `0`,那么就可以使用这个技巧。当合约有一个生命周期状态时,这是最常见的应用场景。
测试
文件位置:
src/storage/Zero.sol
test/storage/Zero.t.sol测试结果:
forge test --gas-report --mt test_zero_ -vvv --optimize
总结
-
该状态变量是内部的,不属于任何外部标准(如 ERC20)。
-
该变量会频繁地在不同状态间切换。
-
它的“闲置”或“默认”状态的数值本身不重要,可以安全地用 1 来代替 0。
避免 ERC20 代币余额为零,始终保持少量。如果一个地址频繁清空(并重新加载)其账户余额,就会导致大量的零到一写入。这里单独提出,因为具体实现起来比较微妙。
ERC20 是一个公开、通用的标准。用户发送了所有 100 个代币,但 balanceOf(his_address) 会返回 1。这会造成灾难性的后果:
-
破坏组合性: 其他 DeFi 协议(如 Uniswap, Aave)依赖于 balanceOf 的准确返回值。如果一个 DEX 认为用户还有 1个代币而实际上用户无法使用它,可能会导致计算错误或交易失败。
-
逻辑错误: 你的合约状态与代币的实际经济价值不符。
-
“灰尘”余额: 用户账户里永远留下了 1 wei 的“灰尘”,他无法将其取出,因为任何尝试转移这 1 wei 的操作都会让余额再次被重置为1。这是一种非常差的用户体验。
-
增加代码复杂性和 Gas 开销,为了实现这个逻辑,你需要在每个 transfer 或 _update 函数中加入额外的代码。
-
在更高层面优化: 如果真的存在一个地址需要频繁进行微小额度的存取,那么问题可能出在应用层设计上。或许应该使用状态通道(State Channels)、或者在 Layer 2 上执行这些高频操作,而不是在 Layer 1 上修改底层的代币合约逻辑。
对应源码
本文配套代码固定到提交
8960383;Gas 数据会随编译器、优化器参数和 EVM 版本变化。