# 加密日记【020】全面掌握Solidity开发

By [0x3c](https://paragraph.com/@gamfi) · 2023-05-15

---

事件

在定义事件的时候，前三个参数可以定义为indexed，方便在区块链里面扫描查找，indexed的参数会存储在topics下面

### **immutable (不可变量)与constant(常量)**

[Solidity 0.6.5](https://learnblockchain.cn/docs/solidity/) 更新引入了一个新的关键字 `immutable`

constant 修饰的变量需要在**编译期确定值**, 链上不会为这个变量分配存储空间, 它会在编译时用具体的值替代, 因此, constant常量是不支持使用运行时状态赋值的(例如:  `block.number` ,  `now` ,  `msg.sender`  等 )。constant 目前仅支持修饰 strings 及 值类型.

immutable 修饰的变量是在**部署的时候确定变量的值**, 所以写在构造函数中。它在构造函数中赋值一次之后,就不在改变, 这是一个运行时赋值, 就可以解除之前 constant 不支持使用运行时状态赋值的限制.

### Solidity 0.6.x更新

1.  继承
    

    contract Part1 {
      constructor() public {}
    }
    contract Part2 {
      constructor() public {}
    }
    contract Child is Part1, Part2 {
      constructor() public {}
    
    }
    

继承顺序是Child⇒ Part2⇒Part，先继承Part2，再继承Part1

在这次更新之后，如果继承的多个合约中有相同的函数，编译器会报错，必须override()父合约，重写函数内容

    pragma solidity ^0.6.10;
    contract A {
        uint public x;
        function setValue(uint _x) public virtual {
            x = _x;
        }
    }
    
    contract B {
        uint public y;
        function setValue(uint _y) public virtual {
            y = _y;
        }
    }
    
    contract C is A, B {
        function setValue(uint _x) public override(A,B) {
            A.setValue(_x);
        }
    

*   只有标记为`virtual`的函数才可以重写它们。 此外，任何重写的函数都必须标记为`override` 。 如果重写后依旧是可重写的，则仍然需要标记为`virtual`
    
*   关键字`super`的工作原理与以前相同：在扁平化继承层次结构中，`super`将函数调用到更上一级的函数。 外部函数（`external`函数）仍然不允许使用`super`
    
*   接口（`interface`）的函数都是隐式虚函数的，因此在实现接口时，必须在实现中显式重写其函数
    

1.  **接口可以继承**
    
    允许接口继承接口。 派生的接口是的所有接口函数的组合。 实现合约必须实现的所有继承接口的函数,如果合约未实现所有函数，则必须将合约标记为`abstract`
    
        pragma solidity ^0.6.10;
        interface X {
            function setValue(uint _x) external;
        }
        
        interface Y is X {
            function getValue() external returns (uint);
        }
        
        contract Z is Y {
            uint x;
            function setValue(uint _x) external override { x = _x; }
            function getValue() external override returns (uint) { return x; }
        }
        abstract contract N is Y {
            uint x;
            function setValue(uint _x) external override { x = _x; }
        }
        
    
2.  抽象合约
    
    0.5版本编译器隐式地将未实现其所有函数的合约当作是抽象合约。0.6必须显式的指定abstract
    
3.  public的返回类型必须一致
    
4.  **不再有状态变量遮蔽**
    
    在继承的合约和本合约中有相同函数名称和变量名称，会报错
    
5.  引入try/catch
    
    *   这是**仅仅提供给外部调用**的特性，部署新合约也被视为外部调用。不可以和低级调用一起使用。
        
    *   该功能能够捕获仅在调用内部产生的异常。调用后的 `try` 代码块是在成功之后执行。不会捕获try 代码块中的任何异常。
        
    *   如果函数调用返回一些变量，则可以在以下执行块中使用它们（如以上示例中所述）。
        
        *   如果执行了 `try` 成功代码块，则必须声明与函数调用实际返回值相同类型的变量。
            
        *   如果执行了低级的`catch`块，则返回值是类型为`bytes`的变量。任何特定条件的`catch`子句都有其自己的返回值类型。
            
    *   请记住，低级`catch (bytes memory returnData)` 子句能够捕获所有异常，而特定条件的`catch`子句只捕获对应的错误。处理各种异常时，请考虑同时使用两者。
        
    *   在为 `try` 外部调用设置特定的gas使用量时，低级的`catch`子句会捕获最终的`out of gas`错误。 但如果交易本身没有足够的 gas执行代码，则`out of gas`是没法捕获的。
        

### Solidity 0.8.x更新

1.  会自动检查溢出，如果默认可以溢出，用unchecked
    
2.  **无效的操作码被还原取代，这样就不会把剩余的gas消耗掉**
    

*   `ABIEncoderV2`现在是默认自动激活。 从0.6开始，Encoder就不再是实验性的了，只是因为遗留的原因，保留了 `pragma experimental` 这个名字。 现在你不需要再加这行了。
    
*   移除任何Openzeppelin SafeMath，你不再需要它了。
    
*   可能需要进行一些类型转换。
    
    *   `msg.sender` 和 `tx.origin` 默认不属于payable类型。 将 `msg.sender.transfer` 改为 `payable(msg.sender).transfer`。
        
    *   只有在符合给定类型的情况下，才允许类型转换，所以`uint256(-1)`将不再工作。 使用`type(uint256).max`代替。
        
    *   当多次改变符号时，类型转换在某些情况下会受到限制，因为类型转换的顺序可能会对结果产生影响。 你现在会看到一个类似TypeError的错误。 不允许从 `int256` 到 `bytes32` 进行显示的类型转换，得先手动转换为uint256。
        
*   修饰组合
    
    *   `myContract.functionCall{gas: 10000}{value: 1 ether }()`改为：
        
    *   `myContract.functionCall{gas: 10000, value: 1 ether }()`。
        
*   将 `x**y**z` 改为`(x**y)**z`，因为默认的执行顺序改变了。
    
*   将 `byte` 类型改为 `byte1` 。
    

[https://docs.soliditylang.org/en/latest/080-breaking-changes.html](https://docs.soliditylang.org/en/latest/080-breaking-changes.html)

### **以太坊合约委托调用（DelegateCall）**

当我们说“函数被调用”时，这意味着我们将特定的上下文(如参数)注入到该组命令(函数)中，并且在此上下文中一个接一个地执行命令。

函数、命令组、地址空间可以通过其名称找到。

在以太坊函数中，调用可以用字节码表示，使用 4 + 32 \* N个字节表达。这个字节码由两部分组成。

*   **函数选择器**：这是函数调用字节码的前4个字节。**函数选择器**是通过对目标函数的名称加上其参数类型(不包括空格)进行哈希（keccak-256哈希函数）取前 4 个字节得到，例如`bytes4(keccak-256(“saveValue(uint)”))`。基于此函数选择器，EVM可以决定应在合约中调用哪个函数。
    
*   **函数参数**：将参数的每个值转换为固定长度为32bytes的十六进制字符串。如果有多个参数，则串联在一起。
    

如果用户将此4 + 32 \* N字节字节代码传递给交易的数据字段。 EVM可以找到应执行的函数，然后将参数注入该函数。

### **停止使用Solidity的transfer()**

以往是因为transfer()和send()是固定gas，用来避免call()的重入攻击，但是现在gas是可变的，这种固定gas的操作就不安全了。

使用[检查-生效-交互(checks-effects-interactions)](https://learnblockchain.cn/docs/solidity/security-considerations.html#checks-effects-interactions)来避免call的重入攻击

### **使用工厂提高智能合约安全性**

工厂模式看成是一种深入防守的托管方式

托管背后的理念是，不同的资金必须分开，以确保合约能够始终覆盖所有的欠款。 要想做好托管，最简单的方法之一就是将资金完全分离到不同的智能合约中。

使用工厂模式的一个主要缺点是，它的Gas 较高。

Solidity 优化
-----------

### **Solidity 怎样写出最节省Gas的智能合约**

1.  **使用短路模式排序Solidity操作**
    
    它将低gas成本的操作放在前面，高gas成本的操作放在后面，这样如果前面的低成本操作可行，就可以跳过（短路）后面的高成本以太坊虚拟机操作了
    
2.  **删减不必要的Solidity库**
    
3.  **精确声明Solidity合约函数的可见性**
    
    [https://yeetsai.com/blog/2018/01/07/solidity-external-public-best-practices/](https://yeetsai.com/blog/2018/01/07/solidity-external-public-best-practices/)
    
    通过显式地标记函数为外部函数（External），可以强制将函数参数的存储位置设置为`calldata`，这会节约每次函数执行时所需的以太坊gas成本
    
4.  **使用适合的数据类型**
    
    *   在任何可以使用`uint`类型的情况下，不要使用`string`类型
        
    *   存储一个uint256要比存储一个uint8的gas成本低，因为每次存储都是32字节的，只存一个uint8还要从256进行缩小，但是如果是多个uint8能组成一个uint256，那比存多个256gas便宜
        
    *   当可以使用`bytes`类型时，不要在solidity合约种使用`byte[]`类型
        
    *   如果`bytes`的长度有可以预计的上限，那么尽可能改用bytes1~bytes32这些具有固定长度的solidity类型
        
    *   bytes32所需的gas成本要低于string类型
        
5.  **避免Solidity智能合约中的死代码**
    
6.  **避免使用不必要的条件判断**
    
7.  **避免在循环中执行gas成本高的操作**
    
8.  **避免使用常量结果的循环**
    
9.  **合并循环**
    
10.  **避免循环中的重复计算**
    
11.  **去除循环中的比较运算**
    

### **控制 gas 成本**

*   **不要存储不必要的数据**
    
*   **将多个小变量打包到单个字中**
    
*   **仅将默克尔根存储为状态**
    
*   **潜在的无限迭代**
    
*   **链外计算(提示)**
    
*   **使用提款模式**
    

### **编写 O(1) 复杂度的可迭代映射**

O(1) 复杂度: 表示即便数量增加，gas 成本也会保持一样。

比如从数组改成链表存储

### **减少智能合约的 gas 消耗的8种方法**

1.  首选数据类型：尽量使用256位的变量，例如 uint256和bytes32
    
2.  在合约的字节码中存储值：constant， [immutable](https://learnblockchain.cn/article/1059)
    
3.  通过SOLC编译器将变量打包到单个插槽中：多个变量凑成一个插槽
    
4.  通过汇编将变量打包到单个插槽中
    
        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)
            }
        }
        
    
5.  连接函数参数
    
        pragma solidity ^0.4.21;
        contract bitCompaction {
          function oldExam(uint64 a, uint64 b, uint64 c, uint64 d) public {
          }
          function newExam(uint256 packed) public {
          }
        }
        Old: func once(uint256 header, uint256 val...) x N
        New: func batch(uint256 header, uint256[] val... x N)
        
    
6.  Merkle 证明可减少存储负载
    
        bytes32 public merkleRoot;
        
        //Let a,...,h be the orange base blocks
        function check
        (
            bytes32 hash4,
            bytes32 hash12,
            uint256 a,
            uint32 b,
            bytes32 c,
            string d,
            string e,
            bool f,
            uint256 g,
            uint256 h
        )
            public view returns (bool success)
        {
            bytes32 hash3 = keccak256(abi.encodePacked(a, b, c, d, e, f, g, h));
            bytes32 hash34 = keccak256(abi.encodePacked(hash3, hash4));
            require(keccak256(abi.encodePacked(hash12, hash34)) == merkleRoot, "Wrong Element");
        
            return true;
        }
        
    
7.  无状态合约
    
    正常的合约
    
        contract DataStore {
            mapping(address => mapping(bytes32 => string)) public store;
            
            event Save(address indexed from, bytes32 indexed key, string value);
            
            function save(bytes32 key, string value) {
                store[msg.sender][key] = value;
                Save(msg.sender, key, value);
            }
        }
        
    
    无状态合约
    
        contract DataStore {
            function save(bytes32 key, string value) {}
        }
        
    
    在 Etherscan 上查看此交易，向下滚动到输入数据，然后单击转换为 Ascii 按钮。我们的数据存在于交易的输入中。
    
    [https://etherscan.io/tx/0xc9fdf51d30e4f9940c57a917a65da5e4ff1e4018283b8e62aa13d2dcea0d9b6b](https://etherscan.io/tx/0xc9fdf51d30e4f9940c57a917a65da5e4ff1e4018283b8e62aa13d2dcea0d9b6b)
    
8.  在IPFS上存储数据
    

### **如何减少字节码大小及节省 gas**

1.  修饰器是内联函数，多次内联可能会超过合约的最大限制24KB，将内联函数的内部代码提取出来改成内部函数
    
        modifier onlyAuthorized() {
                bool isOwner = msg.sender == IOwnable(address(securityToken)).owner();
                require(isOwner ||
                    securityToken.isModule(msg.sender, DATA_KEY) ||
                    securityToken.checkPermission(msg.sender, address(this), MANAGEDATA),
                    "Unauthorized"
                );
                _;
            }
        //改写成
        modifier onlyAuthorized() {
                _isAuthorized();
                _;
            }
        
        function _isAuthorized() internal view {require(msg.sender == address(securityToken) ||
                msg.sender == IOwnable(address(securityToken)).owner() ||
                securityToken.checkPermission(msg.sender, address(this), MANAGEDATA) ||
                securityToken.isModule(msg.sender, DATA_KEY),
                "Unauthorized"
            );
        }
        
    
2.  布尔类型使用8位，而你只需要1位 ，用uint256存储256个布尔值，采用位移的方式
    
3.  当你调用库的公共（public）函数时，该函数的字节码不会包含在合约内，因此可以把一些复杂的逻辑放在库中，这样减小合约的大小。不过你得清楚，调用库会花费一些gas和使用一些字节码。对库的调用是通过委托调用（delegate call）的方式进行的，这意味着库可以访问合约拥有的数据，并且具有相同的权限。因此对于简单任务不值得这样做。
    
4.  无需使用默认值初始化变量
    
5.  使用简短的错误原因字符串，使用error关键字更省gas
    
6.  避免重复检查
    
7.  单行交换变量
    
    (hello, world) = (world, hello)
    
8.  使用事件存储链上不需要的数据
    
9.  适当的使用优化(optimizer)
    
10.  更少调用函数可能更好
    
11.  调用内部函数更便宜
    
    在智能合约内部，调用内部（internal）函数比调用公共（public）函数要便宜，因为当你调用公共函数时，所有参数都会再次复制到内存中并传递给该函数。相比之下，当你调用内部函数时，将传递这些参数的引用，并且它们不会再次复制到内存中。这样可以节省一些 gas ，尤其是在参数较大时如此。
    
12.  **使用代理模式进行大规模部署**
    

### **一些简单的 Gas 优化基础**

1.  升级 Solidity 版本
    
2.  **放弃 Counters.sol**
    
3.  **避免将参数复制到内存**
    
    对于某些类型的参数，如字符串或者数组，Solidity 会强制指定存储位置(`memory`或`calldata` )。这里用`calldata`会便宜的多，所以你会希望尽可能多的使用它，而`memory`只在你需要修改参数时才用（因为`calldata`会让它们只读）
    
4.    
    

当使用任何一种计数器（如`_tokenId`），从1开始而不是从0开始，会让第一次 mint 便宜一些。通常，写入没有值的槽比写入有值的槽更贵。

此外，**整数递增**，`++i`(返回上一次的值，然后再加1)比`i++`(加1，然后返回新的值)更便宜。如果你仅仅需要一个计数器而不需要它的返回值，你可能会更想要第一种。

**除法**，Solidity 插入了一个检查，确保没有被0除。如果你可以确定除数不为0，你可以使用汇编来执行操作，这样可以节省一些额外的gas

标记为`payable`的函数会比其他函数调用时便宜

### **"Stack Too Deep（堆栈太深）" 解决方案**

    // SPDX-License-Identifier: MIT
    pragma solidity 0.7.1;
    
    contract StackTooDeepTest1 {
        function addUints(
            uint256 a,uint256 b,uint256 c,uint256 d,uint256 e,uint256 f,uint256 g,uint256 h,uint256 i
        ) external pure returns(uint256) {
    
            return a+b+c+d+e+f+g+h+i;
        }
    }
    

1.  使用更少的变量
    
2.  利用函数
    
    使用内部函数解决
    
        // SPDX-License-Identifier: MIT
        pragma solidity 0.7.1;
        
        contract StackTooDeepTest1 {
           function addUints(
                uint256 a,uint256 b,uint256 c,uint256 d,uint256 e,uint256 f,uint256 g,uint256 h,uint256 i
            ) external pure returns(uint256) {
        
                return _addThreeUints(a,b,c) + _addThreeUints(d,e,f) + _addThreeUints(g,h,i);
            }
        
            function _addThreeUints(uint256 a, uint256 b, uint256 c) private pure returns(uint256) {
                return a+b+c;
            }
        }
        
    
3.  代码块作用域范围
    
        // SPDX-License-Identifier: MIT
        pragma solidity 0.7.1;
        
        contract StackTooDeepTest2 {
            function addUints(
                uint256 a,uint256 b,uint256 c,uint256 d,uint256 e,uint256 f,uint256 g,uint256 h,uint256 i
            ) external pure returns(uint256) {
        
                uint256 result = 0;
        
                {
                    result = a+b+c+d+e;
                }
        
                {
                    result = result+f+g+h+i;
                }
        
                return result;
            }
        }
        
    
4.  利用结构体
    
        // SPDX-License-Identifier: MIT
        pragma solidity 0.7.1;
        pragma experimental ABIEncoderV2;
        
        contract StackTooDeepTest3 {
            struct UintPair {
                uint256 value1;
                uint256 value2;
            }
        
            function addUints(
                UintPair memory a, UintPair memory b, UintPair memory c, UintPair memory d, uint256 e
            ) external pure returns(uint256) {
        
                return a.value1+a.value2+b.value1+b.value2+c.value1+c.value2+d.value1+d.value2+e;
            }
        }
        
    
5.  一些黑技巧
    
        // SPDX-License-Identifier: MIT
        pragma solidity 0.7.1;
        
        contract StackTooDeepTest4 {
            function addUints(
                uint256 /*a*/,uint256 /*b*/,uint256 c,uint256 d,uint256 e,uint256 f,uint256 g,uint256 h,uint256 i
            ) external pure returns(uint256) {
              return _fromUint(msg.data)+c+d+e+f+g+h+i;
            }
        
            function _fromUint(bytes memory data) internal pure returns(uint256 value) {
                uint256 value1;
                uint256 value2;
        
                assembly {
                    value1 := mload(add(data, 36))
                    value2 := mload(add(data, 68))
                    value  := add(value1, value2)
                }
            }
        }
        
    
    ### **智能合约Gas 优化的几个技术**
    
    *   **交易Gas**：用户每次与智能合约交互时支付的 Gas 量。实现 Gas 高效的函数，必须尽可能地减少Gas消耗。
        
    *   **部署Gas** ：每次部署智能合约时，需要支付的Gas量。部署智能合约通常只发生一次，尽管如此，仍然可以节省 Gas 也是很有趣的。
        

*   **尽量减少链上数据**（使用事件、IPFS、无状态合约、merkle证明）。
    
*   **最小化链上操作**（字符串、返回存储值、循环、本地存储、批处理）
    
*   **内存位置**（calldata、栈、内存、存储）。
    
    以太坊有4个内存位置，从最便宜的到最贵的：calldata，栈，内存和存储。如果使用得当，你将节省大量的**交易 Gas**。
    
    *   **Calldata** ：只适用于**输入参数且参数是外部函数的引用数据类型（数组，字符串 ...）**。Calldata参数是只读的，如果你有一些需要传递给函数的引用类型，总是考虑使用calldata，因为它是最便宜的。
        
    *   **栈**：只对方法中定义的值类型数据有效。
        
    *   **内存**：内存是易丢失的RAM，在EVM终止运行的时候会被移除。你可以用它来存储**引用数据类型**，它比**存储**更便宜。当向其他函数传递参数，或在你的函数中声明临时变量时，除非你严格需要使用存储，否则应该总是使用内存。
        
    *   **存储**：是**最昂贵的**存储位置。存储数据在区块链上持久存在，正如这个列表的第一个元素所说，应该总是尽量减少链上数据
        
*   **变量顺序**
    
*   **首选的数据类型**
    
*   **库**（嵌入式库，独立部署库合约）。
    
*   **最小代理**（Minimal Proxy）
    
*   **构造函数**
    
*   **合约大小**（消息、修改器、函数）。
    
*   **Solidity编译器优化器**
    

[https://learnblockchain.cn/article/4515](https://learnblockchain.cn/article/4515)

### **避免区块Gas限制导致问题**

（2023年3月7日）现在每个区块的目标气体限制为**1500 万气体，但大小可以根据网络需求而变化，最高可达3000 万气体**。 通过调整区块大小和基本费用，网络通常在1500 万气体时达到平衡。 如果区块gas 高于15M 阈值，则提高下一个区块的基本费用。 同样，如果区块gas 低于15M 阈值，则下一个区块的基本费用会降低。

当合约中存在依赖时间、依赖数据大小（如数组长度）的循环时，都可能会有潜在的漏洞。

### **如何缩减合约以规避合约大小限制**

引入这一限制是为了防止拒绝服务（DOS）攻击。

*   **把你的合约分开**
    
*   **使用库**
    
*   **使用代理**
    
*   **删除一些函数**
    
*   **避免额外的变量**
    
*   **缩短错误提示信息**
    
*   **在优化器中考虑一个低的运行值**
    
*   **避免向函数传递结构体**
    
*   **声明函数和变量的正确可见性**
    
*   **删除函数修改器**
    

### **ERC20-Permit**

**之前方法**：

1.  用户提交`token.approve(myContract.address, amount)`交易。
    
2.  等待交易确认。
    
3.  用户提交第二个 `myContract.doSomething()`交易，该交易内部使用 `token.transferFrom`。
    

**现在**：

1.  用户进行授权签名：签名信息`signature=(myContract.address，amount)`。
    
2.  用户向 `myContract.doSomething(signature)`提交签名。
    
3.  `myContract`使用 `token.permit`增加配额，并调用 `token.transferFrom` 获取代币。

---

*Originally published on [0x3c](https://paragraph.com/@gamfi/020-solidity)*
