生活本来就是平凡琐碎的,哪有那么多惊天动地的大事,快乐的秘诀就是不管对大事小事都要保持热情
目录
一、Solidity的特殊变量(全局)
二、Solidity的不可变量
immutable的赋值方式
三、Solidity的事件与日志
事件和日志加深理解
四、Solidity的异常处理
1.require()
2.assert()
3.revert()
4.三者使用例子
5.require、assert、revert区别
6 require、assert 使用场景
7.抛弃的异常语句和其他新增的异常语句
五、Solidity 工厂合约
一、Solidity的特殊变量(全局)
特殊变量,是全局可用的变量,提供关于区块链的信息。下面列出了常用的特殊变量
名称 | 返回 |
---|---|
blockhash(uint blockNumber) returns (bytes32) | 给定区块的哈希值 – 只适用于256最近区块, 不包含当前区块。 |
block.coinbase (address payable) | 当前区块矿工的地址 |
block.difficulty (uint) | 当前区块的难度 |
block.gaslimit (uint) | 当前区块的gaslimit |
block.number (uint) | 当前区块的number |
block.timestamp (uint) | 当前区块的时间戳,为unix纪元以来的秒 |
gasleft() returns (uint256) | 剩余 gas |
msg.data (bytes calldata) | 完成 calldata |
msg.sender (address payable) | 消息发送者 (当前 caller) |
msg.sig (bytes4) | calldata的前四个字节 (function identifier) |
msg.value (uint) | 当前消息的wei值 |
now (uint) | 当前块的时间戳 |
tx.gasprice (uint) | 交易的gas价格 |
tx.origin (address payable) | 交易的发送方 |
二、Solidity的不可变量
Solidity 的不可变量是另一种常量的表达方式。与常量类似,但是不必强制定义就需要赋值,可以在构造函数时传值,部署后无法改变。它是一种修饰符,被它修饰的变量就称之为不可变量
关键字:immutable
immutable 不可变量同样不会占用状态变量存储空间,在部署时,变量的值会被追加的运行时字节码中, 因此它比使用状态变量便宜的多,也同样带来了更多的安全性。
immutable 特性在很多时候非常有用, 最常见的如 ERC20 代币用来指示小数位置的
decimals
变量,它是一个不能修改的变量,很多时候我们需要在创建合约的时候指定它的值,这时 immutable 就大有用武之地, 类似的还有要保创建者地址、关联合约地址等。
immutable的赋值方式
第一种:定义后就赋值
数据类型 修饰符 immutable 不可变量名 = 值; 例如 :address public immutable owner = msg.sender;第二种:构造函数赋值
constructor (参数列表) {
不可变量名 = 值
}
例如:
contract Immutable { address public immutable owner; address public immutable owners; constructor(address _owner) { owner = _owner; owners=msg.sender; }
三、Solidity的事件与日志
Soliddity 事件是以太坊虚拟机(EVM)日志基础设施提供的一个便利接口。是以太坊提供的基本功能,用于将数据记录成日志保存到区块链上,用户可以自定义需要记录的数据,以及topic和索引 ,
事件和日志的区别:
事件强调行为操作、日志强调存储内容,两者是完全不同的概念
事件本质上也相当于一个特殊的函数,称为事件函数
事件在合约中可以被继承
在DAPP的应用中,如果监听了某个事件,当事件发生时,会进行回调执行一系列操作
事件的关键字:event
事件的定义格式:
event 事件名( 所要记录的参数列表 )
事件的触发格式:
emit 事件名( 所要传递给事件的参数列表 )
日志:logs
在以太坊的语境里,日志对事件和触发该事件产生的交易信息的存储
日志的组成:
- address: 交易地址
- args:事件状态变量存储对象(我们所保存的状态变量值就在这里面)
- blockHash: 哈希难度
- blockNumber: 区块号
- event: 事件名
- logIndex:
- removed:
- transactionHash: 交易哈希
- transactionIndex:
日志的作用
- 记录了事件指定的状态变量保存在区块链上的数据(记录了事件不同的状态)
- 通过日志来得到某个事件定义的状态变量中以前和现在的值
注意事项:在web3中采用事件监听所返回的值是该事件的日志,可以通过该日志.args.状态变量名 来获取日志中某个状态变量的值,而这个返回的结果是一个数组,它存储了该状态变量变化的值
当触发了事件,那么remix控制台里面的一个信息里面就有logs(日志)信息
里面记录了ages由值0变成值2
//SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.0;
contract Test{
int age;
string a=unicode'a';
event evt(int ages);
function set(int _age) public returns(string memory){
age=_age;
emit evt(_age);
return a;
}
}
事件和日志加深理解
当定义的事件触发时,我们可以将事件存储到EVM的交易日志中,日志其实是区块链中一种特殊的数据结构
日志与合约关联,与合约合并存储到区块链中,只要某个区块可以访问,其相关的日志也能访问,但在合约中,我们不能直接访问日志和事件数据
可以通过日志实现简单支付验证SPV,如果一个外部实体提供了一个带有这种证明的合约,它可以检查日志是否真实存在区块链中
四、Solidity的异常处理
异常和报错
异常是指程序可以解决的一种错
报错是指程序无法解决的一种错
异常处理
Solidity处理异常和我们常见的语言不一样,solidity是通过回退状态的方式来处理错误,同时给调用者返回一个错误标识。
如果不用回退状态的方式来处理,那么异常后,状态发生错误的改变,在区块链上是需要相当大的代价来完成的
异常处理过程
发生异常时会撤销当前调用(及其所有子调用)所改变的的状态,同时给调用者返回一个错误标识
异常处理模式
早期是if....throw这个会消耗所有的gas
Solidity 0.4.10后发布了新的异常处理方法:异常处理函数
0.6.0版本还出现了try...catch
主要还是以下3种:
- require()
- assert()
- revert()
解决了以前的不好的地方,特别地, assert() 、 require() 代码会 “确保” 提高合约代码l逻辑条理的清晰,所有我们需要好好学习并区别使用它们
1.require()
格式:
require( 判断表达式 , <string>)
<string> 提供了一个自定义错误消息输出的选项
如果不满足条件也就是判断为false,则此函数调用将恢复到原始状态,此函数用于检查输入或外部组件的错误。这个方法一般是用来处理
require 可以有返回值,例如:require(condition, 'Something bad happened');。
返回:'Something bad happened'
require 的返回值不宜过长,因为返回信息需要消耗 gas。
使用 require() 的场景
- 验证一个用户输入是否合法 : require(input>20)
- 验证外部合约的调用结果,例如:require(external.send(amount))
- 判断执行一段语句的前置条件; require(balance[msg.sender]>=amount)
- require应该被经常用到
2.assert()
assert(判断表达式)
如果不满足条件也就是判断为false,则此函数调用将导致一个无效的操作码,对状态所做的任何更改将被还原,这个一般方法是用来处理内部错误的
使用 assert() 的场景
- 检查溢出
- 检查不变量
- 更改后验证状态
- 预防永远不会发生的情况
避免本不应该发生的情况出现,如程序bug
一般来说,使用assert()的频率较少,通常用于函数的结尾。
assert算是最后防线,因为它会在执行的最后来检查行为的合法性
3.revert()
revert()
一执行revert()就将中止执行并将所作的更改还原为执行前的状态
它可以搭配if分支来实现和require和assert的效果
一个交易最终只会有两种状态: cmmit & revert
适用revert的时候
因为该操作是已知不应该出现的时候,所以通常同来检查overflow/underflow、检查被修改过的状态变量是否合法,避免不应该出现的条件发生
4.三者使用例子
uint public num=0;
function testRevert() public {
num++;
if (num>3){
revert(“revert返回的错误”);
}
num++;
}
function testAssert() public {
num++;
assert(num<13);
num++;
}
function testRequire()public {
num++
require( num<23,”require报错信息”);
num++;
}
总结:不管是哪个回退,都是回退到不满足条件前满足条件的状态
5.require、assert、revert区别
require、assert、revert共同点:
assert()与require()语句都需要满足括号中的条件,才能进行后续操作,若不满足则抛出错误。而revert()就抛出错误
以下三个语句的功能完全相同:
// revert if(msg.sender != owner) { revert(); } // require require(msg.sender == owner); // assert assert(msg.sender == owner);require、assert 不同点
assert(false) 编译为 0xfe,这是一个无效的操作码,所以会消耗掉所有剩余的 gas,并恢复所有的操作。
require(false) 编译为 0xfd,这是revert()的操作码,所以会退还所有剩余的 gas,同时可以返回一个自定义的报错信息。
同样作为判断一个条件是否满足的函数,require会回退剩下的gas,而assert会烧掉所有的gas
所以require 的 gas 消耗要小于 assert,而且可以有返回值,使用更为灵活。
6 require、assert 使用场景
- require() 函数用于检测输入变量或状态变量是否满足条件,以及验证调用外部合约的返回值。
- require() 语句的失败报错,应该被看作一个正常的判断语句流程不能通过的事件。
- assert()语句的失败报错,意味着发生了代码层面的错误事件,很大可能是合约中有一个bug需要修复
基本上,require() 应该用于检查条件,而 assert() 只是为了防止发生任何非常糟糕的事情。
7.抛弃的异常语句和其他新增的异常语句
throw的介绍------throw已被废弃
throw在solidity会被编译成invalid opcode,因此执行到这里,EVM会终止tx(交易)且没收所有的gas
Throrw算是一个误用,实际上solidity并没有错误处理的catch机制。因为它在语言上不算好词
官方建议修改用revert()代替
try...catch介绍
我们在当前合约发起对外部合约调用的话,如果外部合约调用执行失败被 revert,外部合约状态会被回滚,当前合约状态也会被回滚。
但有时候我们并不想这样,要是能够捕获外部合约调用异常,然后根据情况做自己的处理不是更好吗?所以,这种场景下适应于使用 try...catch 语句。
和require对比
以下代码将会触发 catch Error(string memory reason) ,最终输出 require error。
pragma solidity ^0.8.0; contract Manager { function count() public pure returns(int){ require(1==2,"require error"); return 2; } function test() public view returns(string memory) { try this.count() { return "success"; } catch Error(string memory reason/* 出错原因 */) { // 调用 count() 失败时执行,通常是不满足 require 语句条件或触发 revert 语句时所引起的调用失败 return reason; } catch (bytes memory) { // 调用 count() 异常时执行,通常是触发 assert 语句或除 0 等比较严重错误时会执行 return "assert error"; } } }
和assert()对比
以下代码将会触发 catch (bytes memory) ,最终输出 assert error。
pragma solidity ^0.8.0; contract Manager { function count() public pure returns(int){ assert(1==2); return 2; } function test() public view returns(string memory) { try this.count() { return "success"; } catch Error(string memory reason/* 出错原因 */) { // 调用 count() 失败时执行,通常是不满足 require 语句条件或触发 revert 语句时所引起的调用失败 return reason; } catch (bytes memory) { // 调用 count() 异常时执行,通常是触发 assert 语句或除 0 等比较严重错误时会执行 return "assert error"; } } }
五、Solidity 工厂合约
Solidity 工厂合约是一种批量部署合约的方式。
通过一个工厂合约创建部署合约,并记录下所有部署合约的地址。
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract Account {
address public bank;
address public owner;
constructor(address _owner) payable{
bank = msg.sender;
owner = _owner;
}
}
contract Factory {
Account[] public accounts;
function createAccount(address owner) external payable{
accounts.push(new Account{value:123}(owner));
accounts.push(new Account{value:456}(owner));
}
}
我们只需要部署 Factory 合约,运行 createAccount 方法,就会自动创建其它合约。