Solidity 基础

Solidity 编程相关

Solidity 是一门类似 JavaScript 的编程语言,用于编写智能合约,与其他智能合约语言相比,它使用最为广泛,相关编程的技巧和误区的研究也较成熟,是入门智能合约编程的首选。

一个简单的合约

pragma solidity >=0.5.9 <0.6.0;
// The Store Contract
contract Store {
uint256 internal value;
function setValue(uint256 v) external {
value = v;
}
function getValue() external view returns (uint256) {
return value;
}
}

如上代码,便是一个最简单的 Solidity 合约。

L1: pragma solidity >0.5.9 表示当前合约应该使用的编译器版本,因为 Solidity 每个版本都有所更新,尤其是有一些语法不兼容的更新。所以需要在合约的开头通过 pragma 限制版本号。版本号限制语法同 npm 。

L3: 注释,注释语法同 JavaScript 。

L4-L14: 合约的主体,所有合约的储存变量接口函数都在此定义。

L5: 这里定义了一个 uint256 类型的合约状态变量 value, internal 表示它是内部的,外部无法直接通过 value 这个名字访问。

L7-L9: 定义了 setValue(uint256) 方法,external 表示它只能被外部调用,即通过区块链相关 API 调用合约的该方法。在方法实现中,状态变量 value, 被设置为函数的入参。

L11-L13: 定义了 getValue() 方法, view 表示它不可以更改合约状态变量, returns 表示其返回类型,合约调用的发起者可以获得该返回值。

由此,我们得到一个合约的标准结构,首先是 pragma 表示编译器版本,其次是定义了合约方法,合约状态变量的合约主体结构。部分较复杂的合约还会在 pragma 之后有 import 语句引入其他库函数。

Solidity 语言基础

基础类型

从上一节的合约例子,我们已经见到了最基础了类型 uint256 . Solidity 还支持如下基础类型:

  • bool : 即 truefalse, Big endian 扩充 256 位。

  • 整数类型: intuint , 支持后缀数字表示位长,最小从 uint8 到最长 uint256. Big endian.

  • address : 地址类型,表示合约或普通账户地址,为 20 字节。内部处理时高位补齐到 32 字节。同时在 TVM 优化中,也处理了 21 字节地址,即支持前缀 0x41

  • 定长字节: bytes1, bytes2, bytes3, …, bytes32. bytebytes1 的别名,高位补齐到 32 字节。

字面常量 - literals

地址,数值。 Solidity 没有八进制数值常量。数值常量可以用科学计数法表示,例如 2.5e1

字符串字面常量。支持单引号或双引号。支持常见转义,支持 \uNNNN 转义。

十六进制字面常量。hex"00112233cafe"

枚举类型 - enum

枚举类型可以通过显式类型转换语法和整数类型互相转换。不支持隐式类型转换。例如 uint(val).

enum ActionChoices { GoLeft, GoRight, GoStraight, SitStill }

由于 ABI 不支持自定义类型,所以出现在 ABI 中的枚举,会自动转换为合适的 uintN 类型。例如 function getChoice() public view returns (ActionChoices) 会变为 getChoice() returns (uint8) .

引用类型 - reference tyes

以上的可以放入一个 uint256 的类型都叫值类型 value type. 与此对应的,是引用类型。

引用类型一般被放入 storagememory 。默认情况下函数参数和返回值中的引用类型是 memory ,合约状态变量是 storage 。(data location)

另外有种特殊的 data location 叫 calldata 隶属于外部函数调用时候传递的参数,类似 memory 但是不可修改。

变长类型 - dynamic size type

bytesstring 这样的长度动态指定的类型,被叫做 dynamic size type. 虚拟机在操作时代价高于定长类型,应避免使用。两者不同的地方在于,虽然底层表示都是字节,但 string 不允许索引操作(utf8 string)。

数组类型 - array

数组可以是编译时确定大小,或动态大小。支持高维数组。例如 uint[][5]

bytes 类型类似 byte[] 但代价更小,需要尽量避免使用 byte[]

创建 array 使用 uint[] memory a = new uint

支持 .length (rw) .push() 操作。

结构体 - struct

struct Funder {
address addr;
uint amount;
}
x = MyStruct({a: 1, b: 2});

java-tron ABI 无法支持结构体的描述,所以出现在 ABI 中的结构体统一表述为 tuple 结构。实际合约中应避免在 ABI 中出现结构体。

参考: StackOverflow

映射 - mapping

即 k-v 哈希字典。mapping(_KeyType => _ValueType) .

key 可以是除 mapping 动态数组,枚举,结构体之外的类型。value 可以是任意类型。

需要注意的是,和其他语言不同, Solidity 中的映射没有 size 的概念,一旦初始化,所有 key 对应的 value 都有默认值(全0值)。所以 mapping 是不可迭代结构。这是由于:

mapping 的内部实现是通过对 key 进行 hash 运算得到一个储存位置,返回这个储存位置对应的值。而对于合约储存而言,储存位置默认都是 0.

类型转换

有了类型,就有类型间的转换。值域属于子集的类型,可以隐式类型转换,否则需要显式类型转换。例如:

int8 a = -3;
uint x = uint(a);

此时, x 将等于 -3 的二进制补码表示。

Storage

合约的 Storage 生命周期同合约本身。同 state variable.

private public external internal

自动为 public storage 变量创建 getter.

表达式和控制结构

程序的功能和逻辑,离不开基础表达式和控制结构。

Solidity 的表达式基本同 C/JavaScript 系语言,相关参考见: 运算符优先级

Solidity 的基本控制结构同样来自 C/JavaScript 系语言,即: if, else, while, do, for, break, continue, return.

此外,对于外部调用和合约创建调用, Solidity 还支持 try/catch 语句。(TRON 目前对合约创建合约的支持较弱)

TRON 目前(4.0.1)只支持合约创建合约的 CREATE 调用, CREATE2 调用并没有实现。

函数

合约的主要逻辑在函数中定义。函数定义的语法如下:

function [name](<parameter types>) {internal|external} [pure|constant|view|payable] [returns (<return types>)] {
function_body
}

如果函数作为类型出现,那么只需要 function (<parameter types>) {internal|external} [pure|constant|view|payable] [returns (<return types>)]

这里的 internal/external 是访问控制关键字,四种访问控制关键字如下:

  • private: only from this contract

  • internal: this contract and derived contract

  • external: can not be accessed internally

  • public: may be called internally

实际使用中,应选择最小范围的访问控制,例如使用 external 而非 public.

  • constant: 不需要网络确认的操作,例如查询 storage, 新版 Solidity 已经废弃了该关键字

  • view: 用于替换原有的 constant 关键字,表示函数不会修改 storage

  • pure: 比 view 更严格,不允许查询 storage

payable 是单独的概念,用于表示该合约函数可以处理代币转账。其中无函数名的特殊 payable 函数叫 fallback function:

function() public payable {}

函数相关操作

this.f.selector: byte4
// internal form
f()
// external form
this.f()

函数修饰符 - function modifier

函数修饰符用于改变函数的行为,例如自动权限检查。

modifier onlyOwner {
require(
msg.sender == owner,
"Only owner can call this function."
);
_;
}
function close() public onlyOwner {
selfdestruct(owner);
}

事件 - Event

Event 通过 EVM/TVM 的 logging 机制,提供了高效的事件记录机制。

contract SimpleAuction {
// 定义事件
event HighestBidIncreased(address bidder, uint amount);
function bid() public payable {
// 触发事件
emit HighestBidIncreased(msg.sender, msg.value);
}
}

合约的构造函数和继承

继承

Solidity 支持合约的多重继承机制。父合约可以定义虚函数,由子合约实现,虚合约,抽象合约。

多重继承不会导致创建多个合约,最终只有一个合约被创建,相关父合约的代码会被 copy 到最终合约。

contract owned {
constructor() { owner = msg.sender; }
address owner;
}
contract mortal is owned {
function kill() {
if (msg.sender == owner) selfdestruct(owner);
}
}
/// ...
contract named is owned, mortal {
constructor(bytes32 name) {
Config config = Config(0xD5f9D8D94886E70b06E474c3fB14Fd43E2f23970);
NameReg(config.lookup(1)).register(name);
}
}
// 继承并提供构造函数
contract PriceFeed is owned, mortal, named("GoldFeed") { ... }

构造函数

默认构造函数 contructor() public {}.

构造函数可以被标记为 publicinternal. internal 相当于抽象合约(类)。

父类构造函数也可以通过如下方式传递:

contract Derived2 is Base {
constructor(uint _y) Base(_y * _y) public {}
}

接口 - interface

接口相当于抽象类。

  • Cannot inherit other contracts or interfaces.

  • Cannot define constructor.

  • Cannot define variables.

  • Cannot define structs.

  • Cannot define enums.

库 - library

库的存在,避免了自己实现轮子的麻烦。保证了多个合约共享逻辑的一致性。

需要区分库和父合约。库其实是一个单独的合约。代码独立与调用者存在,需要通过 DELEGATECALL 指令调用。

库和合约必须单独部署,且库不产生 ABI 文件。库先部署完毕后,得到一个合约地址, 将该合约地址转换为 20 字节地址,填入目标合约 .bin 文件的占位符后。再部署最终合约。

占位符格式在不同的版本的 Solidity 编译器下有所不同。

参考: Libraries.