语法摘要:Contact,数据类型,异常处理
类似 class,可以:abstract、继承和被其他 contract 调用。
典型使用:
* 创建新合约:`new MyContract(…)`
* 使用已部署合约:`MyContract($address)`
### 可见性
| 可见性 | 类似 | 应用于 | 外部可访问 | 子合约可访问 |
| ———- | ———– | —————– | ————- | ————— |
| external | | 函数 | √ | |
| public | public | 函数 + 状态变量 | √ | √ |
| internal | protected | 函数 + 状态变量 | | √ |
| private | private | 函数 + 状态变量 | | |
注:对于 public 变量,会自动生成对应的 getter(详见:[Ethers.js 非权威开发指南(续)](https://mp.weixin.qq.com/s?__biz=MzIyOTY1NDYyMw==&mid=2247484599&idx=1&sn=29bed6f53de918392cac8055681927af&scene=21#wechat_redirect))。
### 关键要素
| 要素 | 说明 | 示例 |
| ———— | —————————————————— | —————————————————————————————— |
| 状态变量 | 永久存储于链上,需耗费 gas | `uint data;` |
| 函数 | 读/写两类,写方法需耗费 gas;可存在于合约内部和外部 | `function func() public {…}` |
| fallback() | 无法直接被外部调用,当请求合约中不存在函数时执行 | `fallback() external { … }` |
| receive() | 无法直接被外部调用,当接收 eth 时执行 | `receive() external payable {…}` |
| modifier | 可复用的声明性约束,函数调用前执行。 | 声明:`modifier onlyOwner(){…}`使用:`function func() public onlyOwner {…}` |
| 事件 | 链上执行日志,可供日后查询。 | `emit Event1(data);` |
| structure | 自定义类型 | `struct MyType { uint item1; bool item2; }` |
| error | 自定义异常 | 声明:`error MyError(unit reason);`使用:`revert MyError(200);` |
| 枚举 | 有限常数值最佳选择 | `enum State { Created, Locked, Inactive }` |
注:
* payable,接收 eth 的函数必需加上
* view 或 pure,表示函数不会改变以太坊状态
* fallback 和 receive 函数
* 都不能有函数名,故没有 `function` 关键字。
* 都必须使用 `external`
* fallback 函数也可以是 payable,但建议优先使用 receive 函数。
* payable fallback 和 receive 函数最多可消耗 2300 gas,这里需重点测试。
* 普通 fallback 则无此限制,只要 gas 足够,任何复杂操作都可以执行。
关于在 dapp 中如何使用事件和查询日志,详见:[Ethers.js 非权威开发指南(下)](https://mp.weixin.qq.com/s?__biz=MzIyOTY1NDYyMw==&mid=2247484557&idx=1&sn=6aa8f9fb9e24505b24e958d524c45799&scene=21#wechat_redirect)
### Interface
类似其他语言中的 interface,可以:
* 继承其他接口
* 只有方法声明,无其他
* 所有方法均为 external
### Library
类似 contract,但:
* 不能用状态变量
* 不能继承或被继承
* 不能接收 eth
* 不能独立执行,必须被其他 contract 引用。
两者关系类似:contract,可执行文件;library,动态链接库
## 数据类型
### 值类型和引用类型
| | 类似 |
| ———- | ———————————————– |
| 值类型 | bool、uint / int、address、byte、enum |
| 引用类型 | 数组(如 bytes / string)、structure、mapping |
注:
* 字节数组和字符串
* `keccak256(abi.encodePacked(s1)) == keccak256(abi.encodePacked(s2))`
* `bytes bs = ‘bytes’;`
* `string str = ‘string’;`
* `byte1 b1 = ‘a’;`
* `byte2 b2 = 256;`,256 超出了单字节大小
* 定长字节数组:byte1 ~ byte32
* 动态字节数据:bytes 和 string,其中 string 为 utf-8 编码。
* solidity 没有提供字符串比较的方法,但可借助 hash 函数完成。
* 数组
* `int[] age = [10, 20, 30, 40, 50];`
* `int[] age = new int[](5);`
* `int[5] age = [10, 20, 30, 40, 50];`
* 定长数组不能用 new 初始化
* 动态数组则能同时使用 new 或初始化赋值
* address 有两种类型:`address` 和 `address payable`,相比前者,后者多了转账功能。
### 存储位置
| | 类似 | 举例 |
| ———- | ————————– | ———- |
| storage | 持久化,合约内全局内存 | 状态变量 |
| memory | 函数的本地内存,非持久化 | 函数入参 |
| calldata | 函数入参,非持久化 | 函数入参 |
| stack | EVM 调用栈 | |
相关规则:
* 优先 calldata,因其可避免拷贝和不可修改
* 函数局部变量:
* `mapping (uint => address) storage localNames = names;`
* 为 storage 时,需指向外部状态变量
* 值类型,memory
* 引用类型,缺省为 storage,但可被指定为 memory。
* mapping,storage,总指向外部状态变量。下例中的 `names` 是定义在合约中的状态变量,类型也是 mapping。
* 赋值规则
* 值类型,产生独立副本
* 引用类型,复制引用
* storage 和 memory / calldata 之间赋值,总产生独立副本。
* memory 变量之间赋值
* storage 向局部 storage 变量赋值,复制引用。
* 其余 storage 赋值,总产生独立副本。
### 全局变量和方法
| | 说明 | 举例 |
| ————- | ——————————————————— | —————————– |
| eth 单位 | wei、gwei、ether | `1 gwei` |
| 时间单位 | seconds、minutes、hours、days、weeks | `1 minutes` |
| block | block 对象 | |
| blockhash() | 若入参为最近 256 个 block 之一,则为其 hash。否则,0。 | |
| msg | msg 对象 | |
| tx | tx 对象 | |
| gasleft() | 剩余 gas | |
| abi | abi 对象 | |
| address | address 对象 | |
| this | 当前合约对象,可显式转换成 address | `address(this).balance` |
| type() | 类型信息 | |
| addmod | (a + b) % k | |
| mulmod | (a * b) % k | |
| 哈希函数 | keccak256、sha256、ripemd160 | |
| ecrecover | 从签名恢复地址 | |
详见:https://docs.soliditylang.org/en/latest/units-and-global-variables.html
注:
* tx.orgin 和 msg.sender 区别
* tx.orgin 为第一个发起 tx 的账户,其值永远是 eoa。
* msg.sender 为当前函数的直接调用账户,可能是 eoa 或 contract address。
* 确保 ecrecover 的第一个参数是有效的 eth 消息签名 hash,可借助 openzepplin 的 ecdsa 工具类完成。
* 优先使用 `address.transfer`,在失败时,`transfer` 抛出异常,而 `sender` 则返回 `false`。
* address 上的低级别方法(call、delegatecall、staticcall、send、transfer)存在两面性:
* 由于缺少运行时的检查,如类型、存在性等,它们执行成本低
* 由此,不安全。
* address 上的 call、delegatecall、staticcall 使用大同小异,但应用场景不同:
* call,应用于 contract
* delegatecall,应用于 library
* staticcall,应用于 contract 只读方法,即 view 或 pure 方法,否则将抛出异常。
* 典型的 address.call 调用:
“`
bytes memory payload = abi.encodeWithSignature(“register(string)”, “MyName”);
(bool success, bytes memory returnData) = address(nameReg).call(payload);
require(success);
“`
* 若需要调整 gas 和发送 eth,则:
“`
address(nameReg).call{gas: 1000000, value: 1 ether}(abi.encodeWithSignature(“register(string)”, “MyName”));
“`
## 异常处理
异常类型:
* Panic,内部错误,如除零错。
* Error,一般异常。
合约抛出异常之后,状态回滚,当前有 3 种方式:
* require(表达式),若表达式为 false,则抛出异常,未使用的 gas 退回
* 适合验证函数的入参,抛出 Error。
* 当前版本的 require 无法和自定义 Error 类型一起使用,如果需要,使用“条件语句 + revert ”组合。
* assert(表达式),同上,但未使用的 gas 不会退回,将全部被消耗
* 适合验证内部状态,抛出 Panic。
* revert(),直接抛出 Error 或自定义 Error,类似其他语言的 throw。
try…catch 语句示例:
“`
try feed.getData(token) returns (uint v) {
return (v, true);
} catch Error(string memory /*reason*/) {
// require 导致
errorCount++;
return (0, false);
} catch Panic(uint /*errorCode*/) {
// assert 导致
errorCount++;
return (0, false);
} catch (bytes memory /*lowLevelData*/) {
// revert 导致
errorCount++;
return (0, false);
}
“`
> 本文首发于:https://mp.weixin.qq.com/s/7UOHvrwUvzbsMH2fLENJZA
Contact
类似 class,可以:abstract、继承和被其他 contract 调用。
典型使用:
- 创建新合约:
new MyContract(...)
- 使用已部署合约:
MyContract($address)
可见性
可见性 | 类似 | 应用于 | 外部可访问 | 子合约可访问 |
---|---|---|---|---|
external | 函数 | √ | ||
public | public | 函数 + 状态变量 | √ | √ |
internal | protected | 函数 + 状态变量 | √ | |
private | private | 函数 + 状态变量 |
注:对于 public 变量,会自动生成对应的 getter(详见:Ethers.js 非权威开发指南(续))。
关键要素
要素 | 说明 | 示例 |
---|---|---|
状态变量 | 永久存储于链上,需耗费 gas | uint data; |
函数 | 读/写两类,写方法需耗费 gas;可存在于合约内部和外部 | function func() public {...} |
fallback() | 无法直接被外部调用,当请求合约中不存在函数时执行 | fallback() external { ... } |
receive() | 无法直接被外部调用,当接收 eth 时执行 | receive() external payable {...} |
modifier | 可复用的声明性约束,函数调用前执行。 | 声明:modifier onlyOwner(){...} 使用:function func() public onlyOwner {...} |
事件 | 链上执行日志,可供日后查询。 | emit Event1(data); |
structure | 自定义类型 | struct MyType { uint item1; bool item2; } |
error | 自定义异常 | 声明:error MyError(unit reason); 使用:revert MyError(200); |
枚举 | 有限常数值最佳选择 | enum State { Created, Locked, Inactive } |
注:
- payable,接收 eth 的函数必需加上
- view 或 pure,表示函数不会改变以太坊状态
-
fallback 和 receive 函数
- 都不能有函数名,故没有
function
关键字。 - 都必须使用
external
- fallback 函数也可以是 payable,但建议优先使用 receive 函数。
- payable fallback 和 receive 函数最多可消耗 2300 gas,这里需重点测试。
- 普通 fallback 则无此限制,只要 gas 足够,任何复杂操作都可以执行。
- 都不能有函数名,故没有
关于在 dapp 中如何使用事件和查询日志,详见:Ethers.js 非权威开发指南(下)
Interface
类似其他语言中的 interface,可以:
- 继承其他接口
- 只有方法声明,无其他
- 所有方法均为 external
Library
类似 contract,但:
- 不能用状态变量
- 不能继承或被继承
- 不能接收 eth
- 不能独立执行,必须被其他 contract 引用。
两者关系类似:contract,可执行文件;library,动态链接库
数据类型
值类型和引用类型
类似 | |
---|---|
值类型 | bool、uint / int、address、byte、enum |
引用类型 | 数组(如 bytes / string)、structure、mapping |
注:
-
字节数组和字符串
keccak256(abi.encodePacked(s1)) == keccak256(abi.encodePacked(s2))
bytes bs = 'bytes';
string str = 'string';
byte1 b1 = 'a';
byte2 b2 = 256;
,256 超出了单字节大小- 定长字节数组:byte1 ~ byte32
- 动态字节数据:bytes 和 string,其中 string 为 utf-8 编码。
- solidity 没有提供字符串比较的方法,但可借助 hash 函数完成。
- 数组
int[] age = [10, 20, 30, 40, 50];
int[] age = new int[](5);
int[5] age = [10, 20, 30, 40, 50];
- 定长数组不能用 new 初始化
- 动态数组则能同时使用 new 或初始化赋值
- address 有两种类型:
address
和address payable
,相比前者,后者多了转账功能。
存储位置
类似 | 举例 | |
---|---|---|
storage | 持久化,合约内全局内存 | 状态变量 |
memory | 函数的本地内存,非持久化 | 函数入参 |
calldata | 函数入参,非持久化 | 函数入参 |
stack | EVM 调用栈 |
相关规则:
- 优先 calldata,因其可避免拷贝和不可修改
-
函数局部变量:
mapping (uint => address) storage localNames = names;
- 为 storage 时,需指向外部状态变量
- 值类型,memory
- 引用类型,缺省为 storage,但可被指定为 memory。
- mapping,storage,总指向外部状态变量。下例中的
names
是定义在合约中的状态变量,类型也是 mapping。
- 赋值规则
- 值类型,产生独立副本
- 引用类型,复制引用
- storage 和 memory / calldata 之间赋值,总产生独立副本。
- memory 变量之间赋值
- storage 向局部 storage 变量赋值,复制引用。
- 其余 storage 赋值,总产生独立副本。
全局变量和方法
说明 | 举例 | |
---|---|---|
eth 单位 | wei、gwei、ether | 1 gwei |
时间单位 | seconds、minutes、hours、days、weeks | 1 minutes |
block | block 对象 | |
blockhash() | 若入参为最近 256 个 block 之一,则为其 hash。否则,0。 | |
msg | msg 对象 | |
tx | tx 对象 | |
gasleft() | 剩余 gas | |
abi | abi 对象 | |
address | address 对象 | |
this | 当前合约对象,可显式转换成 address | address(this).balance |
type() | 类型信息 | |
addmod | (a + b) % k | |
mulmod | (a * b) % k | |
哈希函数 | keccak256、sha256、ripemd160 | |
ecrecover | 从签名恢复地址 |
详见:https://docs.soliditylang.org/en/latest/units-and-global-variables.html
注:
-
tx.orgin 和 msg.sender 区别
- tx.orgin 为第一个发起 tx 的账户,其值永远是 eoa。
- msg.sender 为当前函数的直接调用账户,可能是 eoa 或 contract address。
- 确保 ecrecover 的第一个参数是有效的 eth 消息签名 hash,可借助 openzepplin 的 ecdsa 工具类完成。
- 优先使用
address.transfer
,在失败时,transfer
抛出异常,而sender
则返回false
。 - address 上的低级别方法(call、delegatecall、staticcall、send、transfer)存在两面性:
- 由于缺少运行时的检查,如类型、存在性等,它们执行成本低
- 由此,不安全。
- address 上的 call、delegatecall、staticcall 使用大同小异,但应用场景不同:
- call,应用于 contract
- delegatecall,应用于 library
- staticcall,应用于 contract 只读方法,即 view 或 pure 方法,否则将抛出异常。
- 典型的 address.call 调用:
bytes memory payload = abi.encodeWithSignature("register(string)", "MyName");
(bool success, bytes memory returnData) = address(nameReg).call(payload);
require(success);
- 若需要调整 gas 和发送 eth,则:
address(nameReg).call{gas: 1000000, value: 1 ether}(abi.encodeWithSignature("register(string)", "MyName"));
异常处理
异常类型:
- Panic,内部错误,如除零错。
- Error,一般异常。
合约抛出异常之后,状态回滚,当前有 3 种方式:
-
require(表达式),若表达式为 false,则抛出异常,未使用的 gas 退回
- 适合验证函数的入参,抛出 Error。
- 当前版本的 require 无法和自定义 Error 类型一起使用,如果需要,使用“条件语句 + revert ”组合。
- assert(表达式),同上,但未使用的 gas 不会退回,将全部被消耗
- 适合验证内部状态,抛出 Panic。
- revert(),直接抛出 Error 或自定义 Error,类似其他语言的 throw。
try…catch 语句示例:
try feed.getData(token) returns (uint v) {
return (v, true);
} catch Error(string memory /*reason*/) {
// require 导致
errorCount++;
return (0, false);
} catch Panic(uint /*errorCode*/) {
// assert 导致
errorCount++;
return (0, false);
} catch (bytes memory /*lowLevelData*/) {
// revert 导致
errorCount++;
return (0, false);
}
本文首发于:https://mp.weixin.qq.com/s/7UOHvrwUvzbsMH2fLENJZA
- 发表于 2022-07-11 09:15
- 阅读 ( 286 )
- 学分 ( 1 )
- 分类:以太坊