Solidity 非权威开发指南(2):语法摘要

  • 胡键
  • 更新于 2023-02-07 10:59
  • 阅读 2852

语法摘要:Contact,数据类型,异常处理

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 有两种类型:addressaddress 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

点赞 1
收藏 1
分享
本文参与登链社区写作激励计划 ,好文好收益,欢迎正在阅读的你也加入。

0 条评论

请先 登录 后评论
胡键
胡键
CSM / 架构师 / 创业者,先后就职于中兴和 SAP,现专注于工业物联网、机器学习和区块链。同时,作为机器学习和区块链技术活动的组织者和分享者活跃于本地社区。