DAPP中收取的费用取决于功能逻辑的复杂程度,越复杂消耗的计算资源越多。并且需要用户承担一部分gas,所以solidity 的优化显得非常的重要。同时注重优化gas的合约开发人员写出来的合约代码更安全,质量更高。
1. 封装结构以uint 为例,如果我们的程序中包含多个类似的变量,可以将其封装在一起,因为不管uint8 ,uint32 ,uint16,solidity都会为其保留256位。即使你使用uint8也不会节省gas.
2. 最小化读写链上数据首先明确一点在读写 memory 变量比读写 storage 变量便宜。
contract NotSaveGas {
uint public var1 = 70;
function f1() external view returns (uint) {
uint sum = 0;
for (uint i = 0; i< var1; i++) {
sum += i;
}
return sum;
}
contract SaveGas {
function f2() external view returns (uint) {
uint sum = 0;
for (uint i = 0; i< var1; i++) {
sum += i;
}
return sum;
}
}
请一定要避免f1这种循环读写 storage 变量,这是比较消耗gas的方式。处理这种问题实际可以定义内存变量作为缓存,将数据写入,这样可以节省大量的gas.
3.打开 solidity 优化器hardhat 配置:
module.exports = {
solidity: {
version: "0.8.9",
settings: {
optimizer: {
enabled: true,
runs: 1000,
},
},
},
}
4.尽可能减少链上数据区块链上保存数据是非常昂贵的,所以需要尽可能将链上存储的信息减少,以此来节省大量的交易gas.
使用事件事件是外部事物(例如用户界面)从区块链中获得通知的内置方式。当发出事件时,将通知该事件的监视者。更新合约变量时不会发生通知。事件以不同的方式存储,比使用合约存储便宜得多。合约不能直接访问日志。
IPFS如果你需要存储文件之类,可以使用IPFS保存文件,并将存储的ID保存在链上。
无状态合约 Merkle Proofs如果需要使用区块链来验证一些信息是否有效,可以使用 merkle 证明。Merkle 证明使用单一的数据块来证明更大的数据量的有效性。例如,如果有人想证明 "Tx4 "的有效性,他将需要提供 Tx4、Hash3、Hash12 和 Hash5678,然后你的合约将能够重新计算 Merkle 根(Hash12345678),并检查它是否与存储在区块链上的根相一致。你将不需要存储所有交易的哈希值。
Solidity 存储槽的长度为 32 字节,但并不是所有的数据类型都需要这么大的空间:bool, int8 ... int128, bytes1 ... bytes31 和地址需要的空间小于 32 字节。solidity 会尝试将这些变量打包到同一个存储槽中。
如果你接连定义了 2 个uint128,它们都会被打包到同一个存储槽中,因为它们各占 16 字节。然而,如果你定义了一个uint128,接着是一个unit256,然后是另一个int128,你将使用 3 个存储槽,因为在两个 int128 之间的 unit256 需要一个完整的存储槽。
contract T{
// 不好的方式
uint128 v1;
uint256 v2;
uint128 v3;
// 推荐方式
uint128 v1;
uint128 v3;
uint256 v2;
}
6.选数据类型如果智能合约只需要一个状态变量,一个永远不会大于 255 的无符号整数。我们常规思想可能是想用uint8,会觉得节省gas,实际并不会。以太坊操作码被设计为使用 256 位的变量(EVM 堆栈的大小),而 uint8 只需要 8 位,EVM 会在剩余的位上填上 "0",以便能够操作它。这个由 EVM 执行的填 "0" 操作将花费 Gas,因此为了节省交易 Gas,最好使用 uint256 而不是 uint8。
7.独立部署库如果在智能合约中重复使用代码,最好是将所有的代码打包到一个库中,并通过import的方式指向它。
库包含:
常量和不可变的状态变量在合约被部署后不能被改变。区别在于,常量必须在编译时定义,而不可变量可以在构造函数中定义。总是尽量使用常量,以便使构造函数更便宜。
9.使合约尽可能的小单个合约的限制是24KB,所以要想节省gas,就必须使实现的合约尽可能的小。
modifier TestModifier(uint256 value){
JudgeLength(value);
_;
}
function JudgeLength(uint256 value)internal{
//logic
}
10.最小代理(ERC1167)如果需要部署多个功能完全相同的合约,应该考虑使用 "最小代理"(在ERC 1167中定义)
最小的代理只是一个合约,它将把所有的调用委托给一个预先定义的实现合约。它有一个定义好的字节码,代表最小代理合约的编译代码,你只需要把你的实现合约地址插入其中,你就可以根据需要部署最小代理的多个副本。 参考ERC 1167 相关文章,了解如何使用最小代理)。
由于最小的代理字节码非常小,部署它的成本也低到不能再低,因此节省一堆部署 Gas。
使用最小代理的注意事项,你应该牢记:最小代理的实现合约地址不能改变,这意味着你将不能升级他们的代码。
11.内存位置以太坊存在4个内存位置,从最便宜到最贵的:calldata、stack、memory、storage。
当用户从外部调用一个view函数,是不需要支付一分 gas 的。
这是因为 view 函数不会真正改变区块链上的任何数据 - 它们只是读取。因此用 view 标记一个函数,意味着告诉 web3.js,运行这个函数只需要查询你的本地以太坊节点,而不需要在区块链上创建一个事务(事务需要运行在每个节点上,因此花费 gas)。
在所能只读的函数上标记上表示“只读”的“external view 声明,就能为你的玩家减少在 DApp 中 gas 用量。
注意:如果一个 view 函数在另一个函数的内部被调用,而调用函数与 view 函数的不属于同一个合约,也会产生调用成本。这是因为如果主调函数在以太坊创建了一个事务,它仍然需要逐个节点去验证。所以标记为 view 的函数只有在外部调用时才是免费的。
短路(short-circuiting)是一种使用或/与逻辑来排序不同成本操作的solidity合约 开发模式,它将低gas成本的操作放在前面,高gas成本的操作放在后面,这样如果前面的低成本操作可行,就可以跳过(短路)后面的高成本以太坊虚拟机操作了。
// f(x) 是低gas成本的操作
// g(y) 是高gas成本的操作
// 按如下排序不同gas成本的操作
f(x) || g(y)
f(x) && g(y)
15.删除不必要的库在开发Solidity智能合约时,我们引入的库通常只需要用到其中的部分功能,这意味着其中可能会包含大量对于你的智能合约而言其实是冗余的solidity代码。如果可以在你自己的合约里安全有效地实现所依赖的库功能,那么就能够达到优化solidity合约的gas利用的目的。
例如,在下面的solidity代码中,我们的以太坊合约只是用到了SafeMath库的add
方法:
import './SafeMath.sol' as SafeMath;
contract SafeAddition {
function safeAdd(uint a, uint b) public pure returns(uint) {
return SafeMath.add(a, b);
}
}
通过参考SafeMath的这部分代码的实现,可以把对这个solidity库的依赖剔除掉:
contract SafeAddition {
function safeAdd(uint a, uint b) public pure returns(uint) {
uint c = a + b;
require(c >= a, "Addition overflow");
return c;
}
}
16.精确的声明函数的可见性在Solidity合约开发中,显式声明函数的可见性不仅可以提高智能合约的安全性, 同时也有利于优化合约执行的gas成本。例如,通过显式地标记函数为外部函数(External),可以强制将函数参数的存储位置设置为calldata
,这会节约每次函数执行时所需的以太坊gas成本。
17.避免代码中死代码External 可见性比 public 消耗gas 少。
死代码(Dead code)是指那些永远也不会执行的Solidity代码,例如那些执行条件永远也不可能满足的代码,就像下面的两个自相矛盾的条件判断里的Solidity代码块,消耗了以太坊gas资源但没有任何作用:
function deadCode(uint x) public pure {
if(x< 1 {
if(x >2) {
return x;
}
}
}
18.避免使用常量结果的循环如果一个循环计算的结果是无需编译执行Solidity代码就可以预测的,那么 就不要使用循环,这可以可观地节省gas。例如下面的以太坊合约代码就可以 直接设置num变量的值:
function constantOutcome() public pure returns(uint) {
uint num = 0;
for(uint i = 0; i< 100; i++) {
num += 1;
}
return num;
}
19.合并循环有时候在Solidity智能合约中,你会发现两个循环的判断条件一致,那么在这种情况下就没有理由不合并它们。例如下面的以太坊合约代码:
function loopFusion(uint x, uint y) public pure returns(uint) {
for(uint i = 0; i< 100; i++) {
x += 1;
}
for(uint i = 0; i< 100; i++) {
y += 1;
}
return x + y;
}
20.去除循环中的比较运算如果在循环的每个迭代中执行比较运算,但每次的比较结果都相同,则应将其从循环中删除。
function unilateralOutcome(uint x) public returns(uint) {
uint sum = 0;
for(uint i = 0; i<= 100; i++) {
if(x >1) {
sum += 1;
}
}
return sum;
}
********************
最终的定稿 : https://github.com/blockchainGuide/blockchainguide
公众号 : 区块链技术栈
********************
https://medium.com/coinmonks/smart-contracts-gas-optimization-techniques-2bd07add0e86 本文由博客一文多发平台 OpenWrite 发布!
你是否还在寻找稳定的海外服务器提供商?创新互联www.cdcxhl.cn海外机房具备T级流量清洗系统配攻击溯源,准确流量调度确保服务器高可用性,企业级服务器适合批量采购,新人活动首月15元起,快前往官网查看详情吧