# 加密日记【020】全面掌握Solidity开发 **Published by:** [0x3c](https://paragraph.com/@gamfi/) **Published on:** 2023-05-15 **URL:** https://paragraph.com/@gamfi/020-solidity ## Content 事件 在定义事件的时候,前三个参数可以定义为indexed,方便在区块链里面扫描查找,indexed的参数会存储在topics下面immutable (不可变量)与constant(常量)Solidity 0.6.5 更新引入了一个新的关键字 immutable constant 修饰的变量需要在编译期确定值, 链上不会为这个变量分配存储空间, 它会在编译时用具体的值替代, 因此, constant常量是不支持使用运行时状态赋值的(例如: block.number , now , msg.sender 等 )。constant 目前仅支持修饰 strings 及 值类型. immutable 修饰的变量是在部署的时候确定变量的值, 所以写在构造函数中。它在构造函数中赋值一次之后,就不在改变, 这是一个运行时赋值, 就可以解除之前 constant 不支持使用运行时状态赋值的限制.Solidity 0.6.x更新继承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)的函数都是隐式虚函数的,因此在实现接口时,必须在实现中显式重写其函数接口可以继承 允许接口继承接口。 派生的接口是的所有接口函数的组合。 实现合约必须实现的所有继承接口的函数,如果合约未实现所有函数,则必须将合约标记为abstractpragma 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; } } 抽象合约 0.5版本编译器隐式地将未实现其所有函数的合约当作是抽象合约。0.6必须显式的指定abstractpublic的返回类型必须一致不再有状态变量遮蔽 在继承的合约和本合约中有相同函数名称和变量名称,会报错引入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更新会自动检查溢出,如果默认可以溢出,用unchecked无效的操作码被还原取代,这样就不会把剩余的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以太坊合约委托调用(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)来避免call的重入攻击使用工厂提高智能合约安全性工厂模式看成是一种深入防守的托管方式 托管背后的理念是,不同的资金必须分开,以确保合约能够始终覆盖所有的欠款。 要想做好托管,最简单的方法之一就是将资金完全分离到不同的智能合约中。 使用工厂模式的一个主要缺点是,它的Gas 较高。Solidity 优化Solidity 怎样写出最节省Gas的智能合约使用短路模式排序Solidity操作 它将低gas成本的操作放在前面,高gas成本的操作放在后面,这样如果前面的低成本操作可行,就可以跳过(短路)后面的高成本以太坊虚拟机操作了删减不必要的Solidity库精确声明Solidity合约函数的可见性 https://yeetsai.com/blog/2018/01/07/solidity-external-public-best-practices/ 通过显式地标记函数为外部函数(External),可以强制将函数参数的存储位置设置为calldata,这会节约每次函数执行时所需的以太坊gas成本使用适合的数据类型在任何可以使用uint类型的情况下,不要使用string类型存储一个uint256要比存储一个uint8的gas成本低,因为每次存储都是32字节的,只存一个uint8还要从256进行缩小,但是如果是多个uint8能组成一个uint256,那比存多个256gas便宜当可以使用bytes类型时,不要在solidity合约种使用byte[]类型如果bytes的长度有可以预计的上限,那么尽可能改用bytes1~bytes32这些具有固定长度的solidity类型bytes32所需的gas成本要低于string类型避免Solidity智能合约中的死代码避免使用不必要的条件判断避免在循环中执行gas成本高的操作避免使用常量结果的循环合并循环避免循环中的重复计算去除循环中的比较运算控制 gas 成本不要存储不必要的数据将多个小变量打包到单个字中仅将默克尔根存储为状态潜在的无限迭代链外计算(提示)使用提款模式编写 O(1) 复杂度的可迭代映射O(1) 复杂度: 表示即便数量增加,gas 成本也会保持一样。 比如从数组改成链表存储减少智能合约的 gas 消耗的8种方法首选数据类型:尽量使用256位的变量,例如 uint256和bytes32在合约的字节码中存储值:constant, immutable通过SOLC编译器将变量打包到单个插槽中:多个变量凑成一个插槽通过汇编将变量打包到单个插槽中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) } } 连接函数参数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) 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; } 无状态合约 正常的合约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在IPFS上存储数据如何减少字节码大小及节省 gas修饰器是内联函数,多次内联可能会超过合约的最大限制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" ); } 布尔类型使用8位,而你只需要1位 ,用uint256存储256个布尔值,采用位移的方式当你调用库的公共(public)函数时,该函数的字节码不会包含在合约内,因此可以把一些复杂的逻辑放在库中,这样减小合约的大小。不过你得清楚,调用库会花费一些gas和使用一些字节码。对库的调用是通过委托调用(delegate call)的方式进行的,这意味着库可以访问合约拥有的数据,并且具有相同的权限。因此对于简单任务不值得这样做。无需使用默认值初始化变量使用简短的错误原因字符串,使用error关键字更省gas避免重复检查单行交换变量 (hello, world) = (world, hello)使用事件存储链上不需要的数据适当的使用优化(optimizer)更少调用函数可能更好调用内部函数更便宜 在智能合约内部,调用内部(internal)函数比调用公共(public)函数要便宜,因为当你调用公共函数时,所有参数都会再次复制到内存中并传递给该函数。相比之下,当你调用内部函数时,将传递这些参数的引用,并且它们不会再次复制到内存中。这样可以节省一些 gas ,尤其是在参数较大时如此。使用代理模式进行大规模部署一些简单的 Gas 优化基础升级 Solidity 版本放弃 Counters.sol避免将参数复制到内存 对于某些类型的参数,如字符串或者数组,Solidity 会强制指定存储位置(memory或calldata )。这里用calldata会便宜的多,所以你会希望尽可能多的使用它,而memory只在你需要修改参数时才用(因为calldata会让它们只读) 当使用任何一种计数器(如_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; } } 使用更少的变量利用函数 使用内部函数解决// 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; } } 代码块作用域范围// 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; } } 利用结构体// 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; } } 一些黑技巧// 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避免区块Gas限制导致问题(2023年3月7日)现在每个区块的目标气体限制为1500 万气体,但大小可以根据网络需求而变化,最高可达3000 万气体。 通过调整区块大小和基本费用,网络通常在1500 万气体时达到平衡。 如果区块gas 高于15M 阈值,则提高下一个区块的基本费用。 同样,如果区块gas 低于15M 阈值,则下一个区块的基本费用会降低。 当合约中存在依赖时间、依赖数据大小(如数组长度)的循环时,都可能会有潜在的漏洞。如何缩减合约以规避合约大小限制引入这一限制是为了防止拒绝服务(DOS)攻击。把你的合约分开使用库使用代理删除一些函数避免额外的变量缩短错误提示信息在优化器中考虑一个低的运行值避免向函数传递结构体声明函数和变量的正确可见性删除函数修改器ERC20-Permit之前方法:用户提交token.approve(myContract.address, amount)交易。等待交易确认。用户提交第二个 myContract.doSomething()交易,该交易内部使用 token.transferFrom。现在:用户进行授权签名:签名信息signature=(myContract.address,amount)。用户向 myContract.doSomething(signature)提交签名。myContract使用 token.permit增加配额,并调用 token.transferFrom 获取代币。 ## Publication Information - [0x3c](https://paragraph.com/@gamfi/): Publication homepage - [All Posts](https://paragraph.com/@gamfi/): More posts from this publication - [RSS Feed](https://api.paragraph.com/blogs/rss/@gamfi): Subscribe to updates - [Twitter](https://twitter.com/pangmadee): Follow on Twitter