事件
在定义事件的时候,前三个参数可以定义为indexed,方便在区块链里面扫描查找,indexed的参数会存储在topics下面
Solidity 0.6.5 更新引入了一个新的关键字 immutable
constant 修饰的变量需要在编译期确定值, 链上不会为这个变量分配存储空间, 它会在编译时用具体的值替代, 因此, constant常量是不支持使用运行时状态赋值的(例如: block.number , now , msg.sender 等 )。constant 目前仅支持修饰 strings 及 值类型.
immutable 修饰的变量是在部署的时候确定变量的值, 所以写在构造函数中。它在构造函数中赋值一次之后,就不在改变, 这是一个运行时赋值, 就可以解除之前 constant 不支持使用运行时状态赋值的限制.
继承
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必须显式的指定abstract
public的返回类型必须一致
不再有状态变量遮蔽
在继承的合约和本合约中有相同函数名称和变量名称,会报错
引入try/catch
这是仅仅提供给外部调用的特性,部署新合约也被视为外部调用。不可以和低级调用一起使用。
该功能能够捕获仅在调用内部产生的异常。调用后的
try代码块是在成功之后执行。不会捕获try 代码块中的任何异常。如果函数调用返回一些变量,则可以在以下执行块中使用它们(如以上示例中所述)。
如果执行了
try成功代码块,则必须声明与函数调用实际返回值相同类型的变量。如果执行了低级的
catch块,则返回值是类型为bytes的变量。任何特定条件的catch子句都有其自己的返回值类型。
请记住,低级
catch (bytes memory returnData)子句能够捕获所有异常,而特定条件的catch子句只捕获对应的错误。处理各种异常时,请考虑同时使用两者。在为
try外部调用设置特定的gas使用量时,低级的catch子句会捕获最终的out of gas错误。 但如果交易本身没有足够的 gas执行代码,则out of gas是没法捕获的。
会自动检查溢出,如果默认可以溢出,用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
当我们说“函数被调用”时,这意味着我们将特定的上下文(如参数)注入到该组命令(函数)中,并且在此上下文中一个接一个地执行命令。
函数、命令组、地址空间可以通过其名称找到。
在以太坊函数中,调用可以用字节码表示,使用 4 + 32 * N个字节表达。这个字节码由两部分组成。
函数选择器:这是函数调用字节码的前4个字节。函数选择器是通过对目标函数的名称加上其参数类型(不包括空格)进行哈希(keccak-256哈希函数)取前 4 个字节得到,例如
bytes4(keccak-256(“saveValue(uint)”))。基于此函数选择器,EVM可以决定应在合约中调用哪个函数。函数参数:将参数的每个值转换为固定长度为32bytes的十六进制字符串。如果有多个参数,则串联在一起。
如果用户将此4 + 32 * N字节字节代码传递给交易的数据字段。 EVM可以找到应执行的函数,然后将参数注入该函数。
以往是因为transfer()和send()是固定gas,用来避免call()的重入攻击,但是现在gas是可变的,这种固定gas的操作就不安全了。
使用检查-生效-交互(checks-effects-interactions)来避免call的重入攻击
工厂模式看成是一种深入防守的托管方式
托管背后的理念是,不同的资金必须分开,以确保合约能够始终覆盖所有的欠款。 要想做好托管,最简单的方法之一就是将资金完全分离到不同的智能合约中。
使用工厂模式的一个主要缺点是,它的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成本高的操作
避免使用常量结果的循环
合并循环
避免循环中的重复计算
去除循环中的比较运算
不要存储不必要的数据
将多个小变量打包到单个字中
仅将默克尔根存储为状态
潜在的无限迭代
链外计算(提示)
使用提款模式
O(1) 复杂度: 表示即便数量增加,gas 成本也会保持一样。
比如从数组改成链表存储
首选数据类型:尽量使用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上存储数据
修饰器是内联函数,多次内联可能会超过合约的最大限制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 ,尤其是在参数较大时如此。
使用代理模式进行大规模部署
升级 Solidity 版本
放弃 Counters.sol
避免将参数复制到内存
对于某些类型的参数,如字符串或者数组,Solidity 会强制指定存储位置(
memory或calldata)。这里用calldata会便宜的多,所以你会希望尽可能多的使用它,而memory只在你需要修改参数时才用(因为calldata会让它们只读)
当使用任何一种计数器(如_tokenId),从1开始而不是从0开始,会让第一次 mint 便宜一些。通常,写入没有值的槽比写入有值的槽更贵。
此外,整数递增,++i(返回上一次的值,然后再加1)比i++(加1,然后返回新的值)更便宜。如果你仅仅需要一个计数器而不需要它的返回值,你可能会更想要第一种。
除法,Solidity 插入了一个检查,确保没有被0除。如果你可以确定除数不为0,你可以使用汇编来执行操作,这样可以节省一些额外的gas
标记为payable的函数会比其他函数调用时便宜
// 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 也是很有趣的。
尽量减少链上数据(使用事件、IPFS、无状态合约、merkle证明)。
最小化链上操作(字符串、返回存储值、循环、本地存储、批处理)
内存位置(calldata、栈、内存、存储)。
以太坊有4个内存位置,从最便宜的到最贵的:calldata,栈,内存和存储。如果使用得当,你将节省大量的交易 Gas。
Calldata :只适用于输入参数且参数是外部函数的引用数据类型(数组,字符串 ...)。Calldata参数是只读的,如果你有一些需要传递给函数的引用类型,总是考虑使用calldata,因为它是最便宜的。
栈:只对方法中定义的值类型数据有效。
内存:内存是易丢失的RAM,在EVM终止运行的时候会被移除。你可以用它来存储引用数据类型,它比存储更便宜。当向其他函数传递参数,或在你的函数中声明临时变量时,除非你严格需要使用存储,否则应该总是使用内存。
存储:是最昂贵的存储位置。存储数据在区块链上持久存在,正如这个列表的第一个元素所说,应该总是尽量减少链上数据
变量顺序
首选的数据类型
库(嵌入式库,独立部署库合约)。
最小代理(Minimal Proxy)
构造函数
合约大小(消息、修改器、函数)。
Solidity编译器优化器
https://learnblockchain.cn/article/4515
(2023年3月7日)现在每个区块的目标气体限制为1500 万气体,但大小可以根据网络需求而变化,最高可达3000 万气体。 通过调整区块大小和基本费用,网络通常在1500 万气体时达到平衡。 如果区块gas 高于15M 阈值,则提高下一个区块的基本费用。 同样,如果区块gas 低于15M 阈值,则下一个区块的基本费用会降低。
当合约中存在依赖时间、依赖数据大小(如数组长度)的循环时,都可能会有潜在的漏洞。
引入这一限制是为了防止拒绝服务(DOS)攻击。
把你的合约分开
使用库
使用代理
删除一些函数
避免额外的变量
缩短错误提示信息
在优化器中考虑一个低的运行值
避免向函数传递结构体
声明函数和变量的正确可见性
删除函数修改器
之前方法:
用户提交
token.approve(myContract.address, amount)交易。等待交易确认。
用户提交第二个
myContract.doSomething()交易,该交易内部使用token.transferFrom。
现在:
用户进行授权签名:签名信息
signature=(myContract.address,amount)。用户向
myContract.doSomething(signature)提交签名。myContract使用token.permit增加配额,并调用token.transferFrom获取代币。
