Solidity优化 - 减少智能合约gas消耗

Posted 爱因斯坦小弟

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Solidity优化 - 减少智能合约gas消耗相关的知识,希望对你有一定的参考价值。

1. 首选数据类型

尽量使用 256 位的变量,例如 uint256 和 bytes32!乍一看,这似乎有点违反直觉,但是当你更仔细地考虑以太坊虚拟机(EVM)的运行方式时,这完全有意义。每个存储插槽都有 256 位。因此,如果你只存储一个 uint8,则 EVM 将用零填充所有缺少的数字,这会耗费 gas。此外,EVM 执行计算也会转化为 uint256 ,因此除 uint256 之外的任何其他类型也必须进行转换。

注意:通常,应该调整变量的大小,以便填满整个存储插槽。

2. 在合约的字节码中存储值

一种相对便宜的存储和读取信息的方法是,将信息部署在区块链上时,直接将其包含在智能合约的字节码中。不利之处是此值以后不能更改。但是,用于加载和存储数据的 gas 消耗将大大减少。有两种可能的实现方法:

  1. 将变量声明为 constant 常量 (译者注:声明为 immutable同样也可以降低 gas,测试constant比immutable更加节省gas)
  2. 在你要使用的任何地方对其进行硬编码。
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.11;

contract CryptosTribeToken  
   uint256 add;
   uint256 public v1;
   uint256 public immutable v2=10;

    function calculate() public view returns (uint256 result) 
        return v1 * v2 * 10000;
    

 

3. 通过 SOLC 编译器将变量打包到单个插槽中

当你将数据永久存储在区块链上时,要在后台执行汇编命令 SSTORE。这是最昂贵的命令,费用为 20,000 gas,因此我们应尽量少使用它。在内部结构体中,可以通过简单地重新排列变量来减少执行的 SSTORE 操作量,如以下示例所示:

 

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.11;

contract CryptosTribeToken  
   uint256 add;
   uint256 public v1;
   uint256 public immutable v2=10;

    function calculate() public view returns (uint256 result) 
        return v1 * v2 * 10000;
    
        struct Data 
        uint64 a;
        uint64 b;
        uint128 c;
        uint256 d;
    
    Data public data;
    constructor(uint64 _a, uint64 _b, uint128 _c, uint256 _d)  
        data.a = _a;
        data.b = _b;
        data.c = _c;
        data.d = _d;
    

请注意,在 struct 中,所有可以填充为 256 位插槽的变量都彼此相邻排序,以便编译器以后可以将它们堆叠在一起(也使用占用少于 256 位的那些变量)。在上面的例子中,仅使用两次 STORE 操作码,一次用于存储abc,另一次用于存储d这同样适用于在结构体外部的变量。另外,请记住,将多个变量放入同一个插槽所节省的费用要比填满整个插槽(首选数据类型)所节省的费用大得多

如果将结构体Data中c和d位置调换,那么将使用3次store,a和b一次,c一次,d一次,gas就会币之前多

4. 通过汇编将变量打包到单个插槽中

也可以手动应用将变量堆叠在一起以减少执行的 SSTORE 操作的技术。下面的代码将 4 个 uint64 类型的变量堆叠到一个 256 位插槽中。

编码:将变量合并为一个。

注意:请记得使用编译器打包优化

function encode(uint64 _a, uint64 _b, uint64 _c, uint64 _d) internal pure returns (bytes32 x) 
    assembly 
        let y := 0
        mstore(0x20, _d)
        mstore(0x18, _c)
        mstore(0x10, _b)
        mstore(0x8, _a)
        x := mload(0x20)
    

为了读取,将需要对该变量进行解码,这可以通过第二个功能实现。

解码:将变量拆分为其初始部分。

function decode(bytes32 x) internal pure returns (uint64 a, uint64 b, uint64 c, uint64 d) 
    assembly 
        d := x
        mstore(0x18, x)
        a := mload(0)
        mstore(0x10, x)
        b := mload(0)
        mstore(0x8, x)
        c := mload(0)
    

 

将这种方法的 gas 消耗量与上述方法的 gas 消耗量进行比较,你会注意到,由于多种原因,这种方法的成本明显降低:

  1. **精度:**使用这种方法,就位打包而言,几乎可以做任何事情。例如,如果已经知道不需要变量的最后一位,则可以通过将正在使用的 1 位变量与 256 位变量合并在一起进行优化。
  2. **读取一次:**由于变量实际上存储在一个插槽中,因此只需执行一次加载操作即可接收所有变量。如果变量在一起使用,这将特别有益。

那么,为什么还要使用以前的呢?从这两种实现来看,很明显,我们使用汇编来解码变量,就放弃了代码的可读性,因此,使第二种方法更容易出错。另外,由于每种情况下我们都必须包含编码和解码函数,因此部署成本也将大大增加。但是,如果你确实需要降低函数的 gas 消耗, (与其他方法相比,装入单个插槽中的变量越多,节省的费用就越高。)

区块链-智能合约工程师第三篇:Solidity进阶

文章目录

合约库

库合约一般都是一些好用的函数合集(库函数),为了提升solidity代码的复用性和减少gas而存在。他和普通合约主要有以下几点不同:

  • 不能存在状态变量
  • 不能够继承或被继承
  • 不能接收以太币
  • 不可以被销毁

String库

String库合约是将uint256(大正整数)类型转换为相应的string类型的代码库,主要包含两个函数,toString()将uint256转为string,toHexString()将uint256转换为16进制,再转换为string。

library Strings 
	function toString(uint256 value) public pure returns (string memory) 
	
	function toHexString(uint256 value) public pure returns (string memory) 
	
	function toHexString(uint256 value, uint256 length) public pure returns (string memory) 

调用库函数

使用 using A for B; 语句:添加完指令后,A 库的函数会自动添加为 B 类型变量的函数成员,可以直接调用。(在调用时,B变量会被当作第一个参数传递给函数)

    // 利用using for指令
    using Strings for uint256;
    function getString1(uint256 myNumber) public pure returns(string memory)
        // 库函数会自动添加为uint256型变量的成员
        return myNumber.toHexString();
    

通过库合约名称调用库函数:

    // 直接通过库合约名调用
    function getString2(uint256 myNumber) public pure returns(string memory)
        return Strings.toHexString(myNumber);
    

常用的合约库

合约库说明
String将uint256转换为String
Address判断某个地址是否为合约地址
Create2更安全的使用Create2 EVM opcode
Arrays跟数组相关的库函数

import

solidity支持利用import关键字导入其他源代码中的合约,让开发更加模块化。

  1. 通过相对路径/绝对路径:import ‘./Yeye.sol’;
  2. 通过源文件网址导入网上的合约:import ‘https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/utils/Address.sol’;
  3. 通过npm的目录导入:import ‘@openzeppelin/contracts/access/Ownable.sol’;
  4. 通过全局符号导入特定的合约:import Yeye from ‘./Yeye.sol’;

引用(import)在代码中的位置为:在声明版本号之后,在其余代码之前。

问题:与声明版本号并列,还是在合约里导入?
回答:与声明版本号并列。

接收ETH

Solidity支持两种特殊的回调函数,receive()和fallback(),他们主要在两种情况下被使用:

  • 接收ETH
  • 处理合约中不存在的函数调用(代理合约proxy contract)

接收函数 receive()

receive() 函数只用于处理接收ETH,一个合约最多有一个receive()函数,声明方式与一般函数不一样,不需要function关键字。

声明规则:receive()函数不能有任何的参数,不能返回任何值,必须包含external和payable。

receive() external payable  ... 

我们可以在receive()里发送一个event:

    // 定义事件
    event Received(address Sender, uint Value);
    // 接收ETH时释放Received事件
    receive() external payable 
        emit Received(msg.sender, msg.value);
    

回退函数 fallback()

fallback()声明时不需要function关键字,必须由external修饰,一般也会用payable修饰,用于接收ETH:

fallback() external payable  ... 

定义一个fallback()函数,被触发时候会释放fallbackCalled事件,并输出msg.sender,msg.value和msg.data:

    // fallback
    fallback() external payable
        emit fallbackCalled(msg.sender, msg.value, msg.data);
    

二者的区别

receive和fallback都能够用于接收ETH,他们触发的规则如下,简单说就是:只有msg.data为空且存在receive()时,才会触发receive()

触发fallback() 还是 receive()?
           接收ETH
              |
         msg.data是空?
            /  \\
          是    否
          /      \\
receive()存在?   fallback()
        / \\
       是  否
      /     \\
receive()   fallback()

以上是关于Solidity优化 - 减少智能合约gas消耗的主要内容,如果未能解决你的问题,请参考以下文章

区块链-智能合约工程师第三篇:Solidity进阶

区块链-智能合约工程师第三篇:Solidity进阶

135.003 智能合约后端优化和产品化

智能合约实战 solidity 语法学习 08 [ require assert modifier revert ]

智能合约实战 solidity 语法学习 07 [ require assert modifier revert ] 附代码

Solidity 进阶编程 | 注意一下合约中的细节