针对以太坊智能合约设计实现过程中优化Gas消耗的若干要点总结。
存储相关
- 合理设计合约存储变量,根据场景用途避免不必要的变量持久化,比如适合用于消息或历史记录的数据使用event记录而不是存储变量。
- 合约存储变量值避免频繁地从零变为非零,零变非零耗费20000 gas,比如可以设计总是保持Token合约余额为非零,避免频繁减少为零后又重新变为非零。
- 利用热存储访问,减少冷存储访问(2100 gas),比如使用accessList交易声明提起加载哪些变量进行热访问。
- 使用瞬态存储变量,不同于持久存储变量,瞬态存储只在一次外部交易执行生命周期内有效,gas消耗比持久存储变量少很多,比如可以使用瞬态存储作为防重入保护的标记。
- 业务中值不会改变的合约存储变量应该声明为const或immutable,不会占用存储槽而是直接嵌入到合约字节码。
- 使用栈或内存来对合约存储变量进行缓存,比如一个循环逻辑一直使用存储变量访问可以被改进为在循环体之外使用缓存变量先访问存储变量,然后在循环体内使用该缓存变量。
- 利用打包机制对多个合约存储变量进行打包,比如对于2个uint128变量可以显示使用位操作将它们打包成一个uint256变量只占用一个存储槽,对于结构体变量成员调整其出现的声明顺序可以利用编译器对其进行打包优化减少存储槽占用量。
- 字符串合约存储变量长度尽量控制在小于32范围内,小于32字节的字符串其长度(2*len+1)和字符串本身信息都被存储于同一个存储槽,而大于32字节的字符串其基础槽位存储其长度信息(2*len+1),其他槽位存储字符串本身信息。
计算相关
- 使用uncheck的数学计算,需要谨慎!!!,solidity 0.8.x+已经默认内置整数计算(上/下)溢出检查机制,在保证安全性的同时也耗费更多的gas,在明确无溢出风险时谨慎使用uncheck。
- 使用位移操作代替乘除法,本质也是使用uncheckg。
- 使用mapping代替数组,数组访问时底层会自动检查索引是否溢出从而消耗更多gas,同样需要在保证不会产生溢出访问的情况下谨慎使用。另外可以使用openzeppelin的Array相关库达到该目的。
合约编译部署
- 编译时使用中间表示Yul可以生成更紧凑的合约代码,减少部署运行的代码体积。
- 合约字节码体积上限:EIP-170将合约体积定为24567字节即(~24kB),大型合约需要进行合理组织拆分,使用代理模式等。
- 对于构造函数和只由管理员调用的特权方法,可以将其声明为payable,因为默认无该关键字的方法中内置有检查require(meg.value == 0),因此会有更大的运行字节码并且运行时消耗更多gas。
- 如果合约只用来运行一次,可以在构造方法内使用selfdestruct,将合约代码删除。
- modifer vs 内部函数:使用modifer时会在函数中替换为相应代码,类似C++中的内联,会增加运行合约代码体积,部署消耗gas会更多,但没有函数调用开销,能减少运行时开销;使用内部函数没有代码体积开销,但会增加运行时开销。
- 使用自定义错误 error 代替 require,自定义 error 在 revert 时类似合约方法selector使用其signature的hash前4个字节表示,比require中的字符串信息消耗gas少得多。
- 方法使用external修饰符比public默认更省gas,public方法的大类型(array, mapping, struct, string)参数没有显示使用calldata(public方法参数使用calldata修饰时表示该方法被外部调用时直接使用calldata数据进行只读操作,并且限定合约内部调用该方法时只能使用calldata中的数据作为参数)优化的时候对于来自本合约外的调用会先分配memory然后从calldata中复制参数到memory中做后续处理从而消耗更多gas。对于基础类型参数(uint, bool, address)external和public修饰的gas消耗差异不大。因此对于明显限定只能外部访问的方法,应该优先使用external而不是public。
设计模式
- 对于Airdrop(空投)等需要白名单验证机制的场景,直接使用 mapping(address ⇒ bool) 维护验证白名单代价较高,可利用 Merkle 树证明或签名验证机制来实现白名单以降低 gas 消耗。
- 使用 Merkle 树证明机制时,合约状态只需存储维护一个 Merkle root 值,业务方在链下维护完整的 Merkle 树,白名单用户只需携带 Merkle 证明在链上验证是否与 Merkle root 值一致。整个Merkle树可以公开,用户可以自行生成 Merkle 证明。对于固定白名单场景友好,动态白名单维护成本较高。
- 使用签名验证机制时,合约状态只需要维护一个验证签名的公钥地址值,业务方在链下维护签名私钥以及签名过程,白名单用户只需要携带合法签名在链上进行验证,相比Merkle方案 用户的gas成本低。对于动态白名单,小体量用户友好。
- gas less 交易模式,如ERC20 Permit 的permit函数可接收授权approve者的签名,允许被授权者在一次交易中执行转账操作,将授权者的交易成本转移到被授权者身上。
- 使用layer 2 交易费消耗低,如使用Optimism, Arbitrum, Base等rollup方案,或者使用状态通道合约内锁定资产,链下交易完全没有gas消耗,最后链上结算。
- 使用ERC-1155替代多个ERC-20和ERC-721混合使用的场景,可以单次交易进行多种资产的交易,更省Gas。
- 代理合约模式使用UUPS比透明代理更省代理合约的部署和执行成本,UUPS中代理合约不需要任何多余的方法代码,以及不需要每次方法调用时检查调用者身份角色。
- 考虑比openzeppelin更高效的库,如solady大量使用汇编效率更高。

