Solidity初识

Solidity初识

Solidity是编写智能合约的语言,由于区块链课程需要我们编写Solidity智能合约,我又从来没对Solidity有过了解,因此借这篇博客了解一些有关Solidity的知识。

我们的IDE主要是在线的Remix:https://remix.ethereum.org/

我们也可以在本地安装solc编译器:brew install solidity(mac端)

Smart Contract Compilation In Depth ABI and Bytecode

首先,我们来看看Solidity的编译过程。首先,我们写好智能合约文件,并交给Solidity Compiler 去编译。并得到一个ABI文件和一个Contract Bytecode文件。

  • ABI文件是函数描述符(Application Binary Interface)的缩写,它是智能合约的接口描述,描述了字段名称、字段类型、方法名称、参数名称、参数类型、方法返回值类型等。
  • Bytecode就是字节码,类似于Java 编译后的字节码。有了字节码就可以在任何安装了EVM的机器上运行了。

比如我们编写一个最简单的 智能合约:

1
2
3
4
5
6
7
8
9
10
11
// SPDX-License-Identifier: GPL-3.0

pragma solidity 0.8.7;

contract Property {
int public value;

function setValue(int _value) public{
value = _value;
}
}

在Remix上编译后可以得到其ABI和Bytecode:

  • 下面是一个json格式的文档,从remix上复制下来的,object的这串数字就是这个程序的Bytecode,是以16进制编写的,在本地用solc编译的话只会得Bytecode
  • opcodes是操作代码,有点像汇编。当Bytecode输入到EVM之后,会被翻译成操作码。操作码是程序的低级可读指令,所有操作码都有对应的16进制值,我们可以在这个网站 找到与其对应的值。比如说MUL 代表乘法操作,对应的值为0x02, 消耗的Gas为5。

事实上,我们可以在Etherscan这个网站 来解码 ByteCode,得到Opcode:

现在我们来看看ABI, ABI是以json格式组织的。ABI文件中各参数如下

  • name: 函数名称
  • type:方法类型,包括function, constructor, fallback(缺省方法)可以缺省,默认为function
  • constant:布尔值,如果为true指明方法不会修改合约字段的状态变量
  • payable:布尔值,标明方法是否可以接收ether
  • stateMutability:状态类型,包括pure (不读取区块链状态),view (和constant类型,只能查看,不会修改合约字段),nonpayable(和payable含义一样),payable(和payable含义一样)。其实保留payable和constant是为了向后兼容
  • inputs:数组,描述参数的名称和类型
    • name:参数名称
    • type:参数类型
  • outputs:和inputs一样,如果没有返回值,缺省是一个空数组
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
[
{
"inputs": [
{
"internalType": "int256",
"name": "_value",
"type": "int256"
}
],
"name": "setValue",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [],
"name": "value",
"outputs": [
{
"internalType": "int256",
"name": "",
"type": "int256"
}
],
"stateMutability": "view",
"type": "function"
}
]

Contract Deployment on JS VM

我们在部署界面可以选择合约部署的环境,在这里我们有三个选择:

  • JavaScript VM,这种方式就是在Remix网页上启动一个虚拟区块链环境,然后所有的交易都会在这个虚拟环境(沙箱)中执行。每次刷新页面,JS VM都会重置整个区块链。虽然不是永久的,但是在测试阶段还是非常有用的。
  • Injected Web3, remix会连接一个web3 provider(如MetaMask)并自动获取地址和余额,点击Deploy发布,会在测试网络中发布刚刚编译好的合约,(可能会是10s钟的时间,也可能久一些),当合约部署完毕,我们就可以在区块浏览器上查看到这条合约的详细信息了。当我们想在以太坊主网或者测试网上部署交易,可以选择这种。
  • Web3 Provider,使用这种模式Remix会连接一个远程的以太坊客户端,比如说geth。

这里我们主要使用前两种方式。首先我们看看第一个 JavaScript VM,在我们编译完成之后,我们进入部署页面:

看到默认会生成若干以太坊账户,每个账户中都有100个虚拟的以太币。

然后我们点击Deploy,发现在Deployed Contract中已经有一个合约了,里面有setValue函数接口可以供我们调试。同时我们发现,部署这个合约需要消耗一定量的以太币。

现在我们看看调用setValue会怎么样:我们看到,setValue也需要消耗一定量的代价(这里是43724 gas)

当然我们也可以将一个智能合约部署多次,每个合约的地址都不同,它们之间的变量是不共享、不互通的

Contract Deployment on Rinkeby Using Remix and MetaMask

现在我们用第二种Injected Web3方式来部署合约到Rinkeby测试网络上,这是一种更接近真实情况。首先,我们要注册MetaMask账号,然后通过一些列设置就可以让Remix获取我们的账号地址和账户余额了。

如果我们选择在以太坊主网上部署,是需要钱的,我们自然不可能在这上面部署。

因此我们可以切换网络:

但是我们看到这个账户在Ropsten测试网络一开始也是没有以太币的,我们需要到某些特殊的网站中去乞讨(水龙头网站),比如:

当我领完以太币以后,就可以点击部署了,我们看到部署这个比特币需要0.00029921个Ropsten测试网络上的以太币

我们也可以在 Etherscan 上看到被我们部署上去的合约:

如果我们想调用setValue, 还是需要”花钱的”, 如下所示:

经过了一段时间的等待,在这个过程中需要经过提交、验证等操作,成功以后,我们在EtherScan上可以看到这个交易

在交易未成功的时候,点击value仍然会返回0,但是当交易success之后,点击value就可以返回我们的当时设定的值了。

The Structure of a Smart Contract

现在来介绍一下一个智能合约的结构

  • SPDX版本

首先,在合约的一开始一定要标明 SPDX License Identifier的版本, 如下:

1
// SPDX-License-Identifier: GPL-3.0

因为从Solidity ^ 0.6.8开始,引入了SPDX许可证。因此,你需要在代码中使用SPDX-License-Identifier,虽然它被注释掉了,但是在编译后的Bytecode中还是能被EVM识别到的。如果我们不写的话,会出现Warning。

  • Solidity版本

然后,需要写明这个智能合约用的是solidity的哪个版本。我们可以在 solidity文档 中查看每个版本的更新

1
pragma solidity 0.8.7;

此外,也可以划定 Solidity 版本的一个范围,比如:

1
pragma solidity >=0.5.0 <0.9.0
  • 合约本体

最后就是整个合约的本体了,这就类似于一个class的概念,我们以这个合约为例,看看合约里面有哪些成员

  1. 首先是两个成员变量 price和owner
  2. 然后是一个构造函数,构造函数只会被调用一次
  3. 接下来是一个Function Modifier,也就是函数修饰器。利用函数修饰器可在执行函数之前自动检查条件。比如说,这边changeOwner就调用了这个函数修饰器。是用来检查发起者是否等于当前owner的,如果不是,就会抛出错误信息。
    • 在定义onlyOwner出现的特殊符号 _,是使用该修饰器的函数体插入位置。 ‘_’符号可多次出现,替换成对应的函数体即可。
  4. changeOwner函数和setPrice都是 setter,即给合约的成员变量赋值的。
  5. getPrice函数时getter,用来返回price,因此需要在定义函数后还要写上返回值的类型。
  6. 以上函数都是public的,也就是外部可以调用的(比如python可以就可以通过这些函数调用来获取一些值)

  7. 最后是event(事件),事件是以太坊虚拟机(EVM)日志基础设施提供的一个便利接口。当被发送事件(调用)时,会触发参数存储到交易的日志中(一种区块链上的特殊数据结构)。这些日志与合约的地址关联,并记录到区块链中.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
// SPDX-License-Identifier: GPL-3.0

pragma solidity >=0.5.0 <0.9.0
contract Property{
uint private price;
address public owner;

constructor (){
price =0;
owner = msg. sender;
}
// Function Modifier
modifier onlyOwner(){
require(msg.sender == owner);
_;
}

function changeOwner (address _owner) public onlyOwner {
owner = _owner;
}

function setPrice(uint _price) public {
price = _price;
}
function getPrice() view public returns (uint) {
return price;
}

// Event
event OwnerChanged(address owner);
}

Solidity Basic Syntax Rules

下面来介绍下solidity 的一些基本语法:

  • Solidity是一种高级静态类型的智能合约编程语言,类似于JavaScript。sol是solidity的命令行编译器。
  • Solidity是区分大小写的。
  • 每条语句都必须以分号结尾。

  • 它使用大括号{}来划分代码块的界限。

  • //代表一个单行注释。

  • /*... */代表多行(块)注释。

  • ///代表单行natspec注释,/**...*/代表块状natspec注释。 natspec用于函数声明文档。

  • 大多数控制结构都是可用的:if, else, while, for, break, continue, return。

State and Local Variables

solidity 是显式的语言,因此在声明变量和函数的时候都要注明其类型及其公有还是私有的。

但是变量也是分为 state variables(状态变量)和local variable(本地变量)两种。

  1. State Variale

    • 是在合约层面定义的

    • 使用旧的存储在合约里面的

    • 可以被置为常量,如string constant public location="London"

    • 设置State Variable需要消耗 gas

    • 在声明的时候需要初始化,后续可以使用构造函数或者setter对其进行修改。注意如下情况是不被允许的:

      • ```solidity
        int public price ;
        price = 1;
        1
        2
        3
        4
        5
        6
        7
        8
        9
        10
        11
        12
        13
        14

        2. Local Variable

        + 在函数内部被声明
        + 不消耗gas

        比如下面这个函数 ,在函数里面定义变量和直接对变量 修改都是被允许的。

        ```solidity
        function f1() public pure returns(uint256){
        int x = 5;
        x = x*2;
        return x;
        }

view & pure

在这里还要区分一下两个函数修饰符:view和pure,如上面我们就用pure来修饰f1

  1. View 表示一个函数不能修改状态,在本地执行时并不消耗gas
  2. Pure 表示一个函数不读取状态,也不修改状态

下面几种情况认为是修改了状态:

  • 写状态变量
  • 触发时间
  • 创建其他合约
  • call 调用附加了以太币
  • 调用了任何没有view或pure修饰的函数
  • 使用了低级别的调用(low-level calls)

下面集中情况是读取了状态:

  • 读状态变量
  • 访问了.balance属性
  • 访问了block、tx、msg成员(msg.sig和msg.data除外)
  • 调用了任何没有pure修饰的函数

因此,getter一般需要用view进行修饰,因为他们通常需要读取状态变量,但不涉及写状态变量;但是setter一般不需要view和pure进行修饰,因为setter修改了状态

storage & memory & stack

在solidity合约内部, 函数外部声明的变量默认储存在storage里,函数内部声明的变量默认储存在memory里。那么storage和memory有什么区别呢?

storage memory
储存的变量 函数外部声明的变量,即状态变量 函数内部声明的变量,即局部变量
存储的位置 区块链上,永久存在 内存中,运行完之后销毁(与RAM类似)
运行的位置 区块链网络上 单个节点

但是,比如我想在函数中声明一个字符串变量,如string tmp = "xxx" ,是会报错的。因为string 是一种比较特殊的类型,它不能隐式地转换为预期的字符串存储指针。因此,为了解决这个问题,我们需要显式地定义函数中的字符串,也就是 string memory tmp = "xxx"

其实,solidity 还有一部分空间被称为stack,他存储的是那些在函数中声明的,非引用类型的局部变量(比如int)

stack和memory的区别在于,memory存放的是在函数中声明的且用memory修饰的引用类型的 局部变量。

常见的引用类型有:string、array、struct 和 mapping, 在函数中声明这些类型的变量都需要用memory修饰

Functions, Setters, and Getters

Functions可以理解为是在合约内部的接口。函数类型也是值类型的一种,和C语言中的函数指针类似,用于指向一个函数,可以用于实现回调等功能。

setter

我们现在来讲讲Getter和Setter的定义规范

  • setter:setter需要外部传入信息,因此在函数名称之后要用括号来包含传参,为了和状态变量有所区分,传参前面要加下划线。因为setter需要修改状态,所以不能用view/pure修饰;又是需要外部调用的,所以用public修饰。
1
2
3
4
5
6
7
function setPrice(int _price) public{
price = _price;
}

function setLocation(string memory _location) public{
localtion = _location;
}

getter

  • getter: getter需要向调用者返回信息,因此不需要传参。由于getter只需要访问状态变量,不需要改写状态变量,因此需要用view来进行修饰。同时因为要返回信息,因此在view修饰词之后还要写 return(返回信息的类型)
1
2
3
4
5
6
7
function getPrice() public view returns(int){
return price;
}

function getLocation() public view returns(string memory _location) {
localtion = _location;
}

The Constructor

构造函数我们已经很熟悉了,当合约被创建的时候它就会被调用,且仅被调用一次。默认是public的,我们不用显式声明。

我们看到下面这个构造函数中,出现了owner = msg.sender ,这是什么意思

  • msg.sender,它指的是当前调用者(或智能合约)的 address
    • 在 Solidity 中,功能执行始终需要从外部调用者开始。 一个合约只会在区块链上什么也不做,除非有人调用其中的函数。所以 msg.sender总是存在的。

在这里,由于是我自己发布的合约,因此这里msg.sender就是我的address

1
2
3
4
5
6
7
8
9
10
11
contract Property{
string public location;
int public price;
address public owner

constructor(int _price, string memory _location){
price = _price;
location = _location;
owner = msg.sender;
}
}

immutable&constant

  • Constant

当然,如果我在合约中声明的成员是一个常量,那么就不需要再constructor中对其进行赋值了,但是,在声明的时候就需要对其进行赋值,同时使用constant进行修饰

constant 修饰的变量需要在编译期确定值, 链上不会为这个变量分配存储空间, 它会在编译时用具体的值替代, 因此, constant常量是不支持使用运行时状态赋值的 (例如: block.number , now , msg.sender 等 )

如下:

1
int constant area = 100;
  • Immutable

immutable 修饰的变量是在部署的时候确定变量的值, 它在构造函数中赋值一次之后,就不在改变, 这是一个运行时赋值, 就可以解除之前 constant 不支持使用运行时状态赋值的限制.

immutable不可变量同样不会占用状态变量存储空间, 在部署时,变量的值会被追加的运行时字节码中, 因此它比使用状态变量便宜的多, 同样带来了更多的安全性(确保了这个值无法在修改).

可以这样理解:constant是声明时候赋值,不能经constructor赋值;immutable可以不在声明时赋值,可以由constructor赋值,之后就不能再改变。

变量类型

Solidity是静态类型语言,也就是数据类型是在编译期间就决定的,和C++、Java是一样的。solidity编程语言提供了一些基本类型(simple types)可以用来组合成复杂类型。

值类型

值类型包含

  • 布尔(Booleans)
  • 整形(Integer)
  • 地址(Address)
  • 定长字节数组(fixed byte arras)
  • 有理数和整形(Rational and Integer Literals,String literals)
  • 枚举类型(Enums)
  • 函数(Function Types)

为什么会叫值类型,是因为上述这些类型在传值时,总是值传递。比如在函数传参数时,或进行变量赋值时。

引用类型(Reference Types)

复杂类型,占用空间较大的。在拷贝时占用空间较大。所以考虑通过引用传递。常见引用类型有:

  • 不定长字节数组(bytes)
  • 字符串(string)
  • 数组(Array)
  • 结构体(Structs)

布尔(Booleans)

bool:可能的取值为常量true 和 false
支持的运算符:

  • ! 逻辑非
  • &&逻辑与
  • ||逻辑或
  • ==等于
  • !=不等于
    注意:&&|| 是短路运算符,他只会先执行前面的,如果无法判定结果才会执行后面的,比如 f(x) || g(y),如果f(x)已经判定为false,则结果为false,不会再执行g(y);同理 f(x) && g(y)f(x)判定为真,则 g(y)也不会再执行。

如果声明的布尔变量未被初始化,那么其会被默认置为false

整形(Integer)

int/uint: 变长的有符号或无符号整形.变量支持的步长以8递增,支持从 uint8uint256,以及 int8 到 int256。需要注意的是,uintint 默认代表的是 uint256($0\sim2^{256}$) 和 int256($2^{-128}\sim 2^{127}$)

支持的运算符:

  • 比较: <= , < , ==, != , >=, >, 返回值为bool类型。
  • 位运算符: &| , (^异或) , (~非).
  • 数学运算: + , -, * ,/, (%求余), ( **幂)

整数除法总是截断的,但如果运算符是字面量,则不会截断(后面会进一步提到).另外除 0 会抛出异常.

如果声明了整型但不对其进行初始化,那么会被默认置为0;

定长数组 Fixed-Size Arrays

现在我们来介绍定长数组,在编译的时候它的长度已经被确定下来了。

成员变量
.length 表示这个字符数组的长度(只读)

固定大小字节数组可以通过 bytes1, bytes2, bytes3, …, bytes32来进行声明。PS:byte的别名就是 byte1

  • bytes1只能存储一个字节,也就是二进制8位的内容。
  • bytes2只能存储两个字节,也就是二进制16位的内容。
  • bytes3只能存储三个字节,也就是二进制24位的内容。
    ……
  • bytes32能存储三十二个字节,也就是二进制32 * 8 = 256位的内容。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    contract C {

    // 0x6c697975656368756e

    byte public a = 0x6c; // 0110 1100
    bytes1 public b = 0x6c; // 0110 1100
    bytes2 public c = 0x6c69; // 0110 1100 0110 1001
    bytes3 public d = 0x6c6979; // 0110 1100 0110 1001 0111 1001
    bytes4 public e = 0x6c697975; // 0110 1100 0110 1001 0111 1001 0111 0101

    // ...

    bytes8 public f = 0x6c69797565636875; // 0110 1100 0110 1001 0111 1001 0111 0101 0110 0101 0110 0011 0110 1000 0111 0101
    bytes9 public g = 0x6c697975656368756e; // // 0110 1100 0110 1001 0111 1001 0111 0101 0110 0101 0110 0011 0110 1000 0111 0101 0110 1110
    }

使用这种方法来定义的话,无法对特定的位数进行赋值,比如a[0]='a' 是不可以的

而且这样很不直观,我们可以像定义普通数组一样来声明定长数组

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
contract FixedSizeArrays{
uint[3] public numbers;//未初始化,就都为0
uint[3] public numbers3 = [2,3,4];//初始化

/*
setter 可以调用此函数为数组中的某个元素赋值
*/
function setElement(uint index,uint value) public{
numbers[index] = value;
}

/*
getter 获取数组长度
*/
function getLength() public view returns(uint) {
return numbers.length;
}
}

当我们访问越界的时候,就会报错

变长数组 Dynamically-Sized Arrays

变长数组顾名思义可以改变是数组的长度, 类似于一个栈,后进先出。

成员变量

.length 表示这个字符数组的长度

.push 表示往这个数组中添加成员

.pop 表示将数组中某成员删除

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
contract DynamicArrays{
uint[] public numbers;

function getLength() pubilc view returns (uint){
return numbers.length;
}

function addElement(uint item)public {
numbers.push(item);
}

function popElement() public {
numbers.pop();
}

function getElement(uint i) public view returns (uint){
if(i < numbers.length){
return numbers[i];
}
return 0;
}
}

我们也可以定义局部变量数组。 需要注意的是,array属于引用类型,需要显式用memory修饰。和Storage Array不同,Memory Array无法使用pop和push,而且也无法使用.length得到长度。因此,如果要将Memory Array赋值给Storage Array的话,需要在声明时注明数组的长度。

1
2
3
4
5
6
7
function f() public {
uint[] memory y = new uint[](3);
y[0] = 10;
y[1] = 20;
y[2] = 30;
numbers = y;
}

Bytes and String Types

字符串常量是指由单引号,或双引号引起来的字符串("foo"or 'bar')。solidity字符串并不像C语言一样包含结束符,”foo“这个字符串大小仅为3字节。和整数常量一样,字符串的长度类型可以是变长的。字符串可以隐式的转换为byte1…byte32如果合适,也会转为bytes 或 String。

字符串常量支持转义字符,比如\n,\xNN,\uNNN。其中\xNN表示16进制值,最终转换合适的字节.而\uNNNN表示Unicode编码值,最终会转换为UTF8的序列.

现在我们来看一下Bytes和String的区别,比如我都将其初始化为'abc',但实际上bytes变量是按照ASCII码存储的,而string则是由UTF-8格式存储的

1
2
3
4
contract DynamicArrays{
bytes public b1 = 'abc';
string public s1 = 'abc';
}

Structs and Enums

  • Struct

Solidity中的Struct与C语言中的struct类似。是一个键值对的集合,类似于映射,但值可以有不同的类型。

struct 会被存储在storage里面。一般来说,struct是定义在contract外的。下面我定义了一个Instructor 结构,然后在智能合约中通过constructor进行初始化。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
struct Instructor {
uint age;
string name;
address addr;
}

contract Academy{
Instructor public academyInstructor;

constructor (uint _age , string memory _name){
academyInstructor.age = _age;
academyInstructor.name = _name;
academyInstructor.addr = msg.sender;
}

/*
此外,如果我们需要修改状态变量academyInstructor,需要传入struct所需的全部参数。
而且,由于struct是引用变量,因此需要用memory显式修饰。
*/
function changeInstructor(uint _age, string memory _name ,address _addr) public{
Instructor memory myInstructor = Instructor({
age: _age,
name: _name,
addr: _addr
});
academyInstructor = myInstructor;
}
}
  • Enum

Enum和java中的Enum类似,可以用于定义取值范围有限的类型。Solidity 中Enum可以和整形显式的相互转换,整形再转换成enum时,编译器/EVM会检查取值范围,如果范围有误则会产生一个错误。 下面是一个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
contract EnumTest {

event UintValue(uint value);
event EnumValue(Status status);

enum Status {ACTIVE,SUSPENDED}

function enumTest() public {
Status s1 = Status.ACTIVE;
//0 will be emited
emit UintValue(uint(s1));

Status s2 = Status(1);
//1 will be emited
emit EnumValue(s2);

//Next line will get an compile time error for 2 is out of range
//Status s2 = Status(2);

uint x = 4 - 4;
Status s3 = Status(x);
//0 will be emited
emit EnumValue(s3);

// x = 4 - 2;
//The next line will get an run time error for x is out of range
// Status s4 = Status(x);
}
}

Mappings

solidity里的映射可以理解为python里的字典,建立键-值的对应关系,可以通过键来查找值,键必须是唯一的,但值可以重复。

定义方式为:mapping(键类型=>值类型),例如mapping(address=>uint) public balances,这个映射的名字是balances,权限类型为public,键的类型是地址address,值的类型是整型uint,在solidity中这个映射的意思是将参数amount的值和msg.sender这个地址对应起来。

在solidity中,一般有如下性质

  • 所有的键都必须是一个类型的;所有的值也必须是同一类型的
  • Mapping永远是被存放在storage中的,它是状态变量。就算是在函数中定义的mapping也会被存放在storage里
  • Mapping的优势在于查找过程是$O(1)$的,相比与线性查找的数组,要快很多。
  • Mapping并不是可迭代的,也没有一个迭代器可以遍历mapping
  • 如果我们用一个不存在的key去mapping查找,就会得到一个默认的值
1
2
3
4
5
6
7
8
contract Auction{
mapping(address => uint) public bids;

function bid() payable public {
bids[msg.sender] = msg.value;
}

}

Overflows and Underflows

在早期的solidity版本中,整型变量是会发生上溢和下溢的,比如,我把solidity版本置为0.5.0, 然后定义一个uint8=255的变量,利用一个函数调用让其发生上溢:

1
2
3
4
5
6
7
8
9
10
pragma solidity ^0.5.0;

contract Property{
uint8 public x = 255;

function f1() public{
x += 1;
}

}

如上所示,我若将uint8类型的255加上1,不会报错,但是数值从255变为了0

在比较新的solidity版本中(比如0.8.0),当我使用同样的代码时,调用该函数会报错,x保持255的值不变。这是因为在新版本的solidity对可运算的数值类型加入了自动检查的功能。如果我们偏要忽略运算检查,可以使用unchecked来修饰,比如:

1
2
3
function f1() public{
unchecked {x += 1};
}

此外,还有一个显著的差别就是:bytes变量可以调用push函数插入字符、可以用pop弹出字符、可以用length获取长度,但是string都不行。

1
2
3
4
5
6
7
8
contract DynamicArrays{
bytes public b1 = 'abc';
string public s1 = 'abc';

function addElement() public {
b1.push('x');
}
}

需要注明的是,相比于变长数组,定长数组消耗更少的gas,因此能用则用。

Built-In Global Variables

现在我们要学习一些solidity 内建的全局变量。其实之前我们已经接触到了这些变量,比如msg系列的、block系列的

abi

  • abi.encode(...) returns (bytes):对给定的参数进行ABI编码。
  • abi.encodePacked(...) returns (bytes): Performes packed encoding of the given arguments
  • abi.encodeWithSelector(bytes4 selector, ...) returns (bytes)::对给定的参数进行ABI编码——从第二个预置给定的四字节选择器开始
  • abi.encodeWithSignature(string signature, ...) returns (bytes):相当于abi.encodeWithSelector(bytes4(keccak256(signature), ...)

block系列

  • blockhash(uint blockNumber): 给定的块的hash值, 只有最近工作的256个块的hash值
  • block.coinbase (address): 当前块的矿工的地址
  • block.difficulty (uint): 当前块的难度
  • block.gaslimit (uint): 当前块的gaslimit
  • block.number (uint):当前块的数量
  • block.timestamp (uint):当前块的时间戳,注:不要依赖于block.timestampnowblockhash用作随机性的来源,除非你知道你在做什么。
1
2
3
4
5
6
7
contract Academy{
uint public this_moment = block.timestamp;
uint public block_number = block.number;
uint public difficulty = block.difficulty;
uint public gaslimit = block.gaslimit;
address public coinbase = block.coinbase;
}

在这里,由于是在JS VM上跑的,所以block_number并不是真实的。

msg系列

  • msg.data(bytes): 完整的calldata

  • msg.sender (address): 消息的发送者(当前调用)

  • msg.value (uint): 和消息一起发送的wei的数量

    • ```solidity
      function sendEther() public payable{
          sendValue = msg.value;
      
      }

其他

  • now (uint): 当前块的时间戳(block.timestamp的别名)

  • gasleft() returns (uint256): 剩余 gas

    • ```solidity
      function howMuchGas() public view returns(uint){
          uint start = gasleft();
          uint j = 1;
          for (uint i = 1;i<20;i++){
                  j*=1;
          }
          uint end = gasleft();
          return start-end;
      
      }
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      31
      32
      33
      34

      通过`gasleft()`我们可以 获取一段代码消耗了多少gas

      <img src="./Solidity初识/18.png" />

      - `tx.gasprice` (`uint`):交易的gas价格
      - `tx.origin` (`address`):交易的发送者(全调用链)
      - `assert(bool condition)`: abort execution and revert state changes if condition is `false` (用于内部错误)
      - `require(bool condition)`: abort execution and revert state changes if condition is `false` (用于输入错误或外部组件的错误)
      - `require(bool condition, string message)`: abort execution and revert state changes if condition is `false` (用于输入错误或外部组件的错误). 并提供错误信息.
      - `revert()`: 中止执行并还原状态更改
      - `revert(string message)`:中止执行并还原状态更改,提供解释字符串
      - `blockhash(uint blockNumber) returns (bytes32)`: : 给定的块的hash值, 只有最近工作的256个块的hash值
      - `keccak256(...) returns (bytes32)`:计算(紧凑排列的)参数的 Ethereum-SHA3 hash值
      - `sha3(...) returns (bytes32)`: an alias to `keccak256`
      - `sha256(...) returns (bytes32)`: 计算(紧凑排列的)参数的SHA256 hash值
      - `ripemd160(...) returns (bytes20)`:计算 256个(紧凑排列的)参数的RIPEMD
      - `ecrecover(bytes32 hash, uint8 v, bytes32 r, bytes32 s) returns (address)`: 椭圆曲线签名公钥恢复,错误时返回0

      - `this` (current contract’s type): 当前合约,在地址上显式转换

      - `super`: 在层次关系上一层的合约

      - `selfdestruct(address recipient)`: 销毁当前的合约,将其资金发送到指定`address`

      - `suicide(address recipient)`: a deprecated alias to `selfdestruct`

      - `<address>.balance` (`uint256`): address地址中的账户余额(以wei为单位)

      - ```solidity
      // 当前合约的剩余额(单位是wei)
      function getBalance() public view returns(uint) {
      return address(this).balance
      }
  • <address>.send(uint256 amount) returns (bool): 将一定量wei发送给address地址,若失败返回false
  • <address>.transfer(uint256 amount): 将一定量wei发送给address地址,若失败抛出异常。

Contract’s Address and Balance Payable, Receive and Fallback Functions

在学习以太坊的时候,我们知道账户也分为两类:外部账户和 合约账户

  • 外部账户(EOAs) 由公钥-私钥对 控制,它拥有私钥,没有相关代码,其codeHash为空。
  • 合约账户由交易类型、消息类型进行创建,由代码控制,简称CA,它没有私钥,其codeHash非空

因此,我们需要去理解的是,合约也是一种账户,也有它自己的地址(在部署的时候生成)。因此,合约也可以接收以太币、支付以太币。

但是需要注意,有两种合约地址类型:plain和payable。前者我们无法向其发送以太币,后者我们可以向其发送以太币。

同时,我们也要知道 address是一种特殊的变量类型,他有很多成员可供调用:

  • balance: 余额

  • transfer(): 是一种最安全的方法用于发送ETH

  • send() :类似于低配版的transfer,当执行失败的时候,合约并不会停止,而且send会返回false。transfer()send() 只有 payable address才可以使用

payable functions and contract balance

只有当payable function被定义了,这个合约才可以收取ETH并有ETH余额。

一个合约可以通过多种方法获取ETH:

  • 用外部账户(EOA) 向一个合约地址转ETH。在这种情况下,合约至少需要定义receive()或者fallback()函数中的一个
    • fallback函数的性质如下:
      • 三无函数。没有名字、没有参数、没有返回值。
      • 替补函数。如果在一个对合约调用中,没有其他函数与给定的函数标识符匹配fallback会被调用. 或者在没有 receive 函数时,而没有提供附加数据对合约调用,那么fallback 函数会被执行。
      • 收币函数。通过钱包向一个合约转账时,会执行Fallback函数,这一点很有用。
      • fallback 函数始终会接收数据,但为了同时接收以太币,必须标记为 payable
    • receive函数性质如下:
      • 一个合约最多有一个 receive 函数, 声明函数为: receive() external payable { ... }
      • 不需要 function 关键字,也没有参数和返回值并且必须是 external 可见性和 payable 修饰. 它可以是 virtual 的,可以被重载也可以有 修改器modifier 。
      • 在对合约没有任何附加数据调用(通常是对合约转账)时会执行 receive 函数。如果receive函数不存在,但是有fallback函数。那么在进行纯以太币转账的时候,fallback会被调用
      • 如果两个函数都没有,这个合约就没法通过常规的转账交易接收以太币(会抛出异常).
  • 调用payable function,并在那个交易中附带着发送ETH
1
2
3
4
5
6
7
8
9
10
11
12
contract Deposit{
receive() external payable{
}

fallback() external payable{
}

function getBalance() public view returns(uint){
return address(this).balance;
}

}

为了调试这个合约,我们需要将其在以太坊测试网络上进行部署:上链之后,我们看到这个合约地址已经生成了。

在Remix上,我们可以调用getBalance来看合约账户的余额,由于没有为其转账,因此余额为零

方式1: 外部账户给合约账户直接转账

现在,我到 MEW 网站上又注册了一个账号,并向这个合约地址转账了0.2个ETH,然后,我们就可以看到

调用合约内部的payable function

当然,我们也可以采取第二种方式调用合约内部的 payable function来对合约进行转账。

首先我们要创建一个payable function:

1
2
3
4
function sendEther() public payable{
uint x;
x++;
}

然后,我们需要在value中设置需要转账的钱, 1Finney = 0.001ETH

点击sendEther按钮之后,就会转接到metamask界面进行转账操作了。

Accessing the Contract’s Balance

前面我们学习了如何向一个合约转账,现在我们要来学如何获取一个合约账户的余额,并向其它合约账户、外部账户转账。

为此,我们要设计一个transfer函数用来实现账户之间的转账。

1
2
3
4
5
6
7
8
function transferEther(address payable recipient,uint amount)public returns(bool){
if(amount <= getBalance()){
recipient.transfer(amount);
return true;
}else{
return false;
}
}

部署以后我们往这个账户中转一些以太币以供后续测试用。然后,我们要给上面的那个合约转账的话,我们只需在transferEther里面输入目标地址和转账数目即可。

值得注意的是,这仅仅涉及合约与合约之间的转账,但是转账交易需要消耗一定的燃料,这仍然是需要EOA来支付的。

Protecting the Contract’s Balance

现在有一个问题,就是说我们这个transferEther函数时完全公开的,外部的人只要能获取合约地址就可以调用这个函数,因此是非常不安全的。因此对一个合约我们要保护好其余额.

为此我们可以使用 require 函数,它可以在执行函数逻辑之前做一些检查。这里我们首先创建一个owner状态变量,然后为其创建构造函数并为其赋值。因此,当这个合约被部署的时候,owner就会被设定为部署者的账号。然后,当要调用transferEther函数的时候,会调用require(owner == msg.sender) 进行核查,如果调用者不是owner,那么就会直接返回false。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
contract Deposit{
address public owner;

constructor(){
owner = msg.sendEther;
}

receive() external payable{

}
fallback() external payable{

}

function getBalance() public view returns(uint){
return address(this).balance;
}

function sendEther() public payable{
uint x;
x++;
}

function transferEther(address payable recipient,uint amount)public returns(bool){
require(owner == msg.sender,"Transfer failed ,you are not the owner");
if(amount <= getBalance()){
recipient.transfer(amount);
return true;
}else{
return false;
}
}
}

我们可以来做个实验, 我们用账户1部署好合约之后,切换到账户2,然后进行转账活动

Variables and Functions Visibility Private, Public, Internal, External

函数类型分为internalexternal, 标记有internal函数类型只能引用当前contract中的函数,标记有external函数类型可以应用定义在其他contract中的函数。

除此之外,还有一个payable 修饰词 ,如果在函数中涉及到以太币的转移,需要使用到payable关键词。意味着可以在调用这笔函数的消息中附带以太币。可以参考这篇博客

对于函数和状态变量,有四种类型的可见性。

  • public

    • 该函数可以在内部(从同一合约内)和外部(从其他合约或EOA账户)调用。
    • 创建一个函数,默认为public
    • 一个getter被自动创建为公共变量。它们可以很容易地从dApps中访问。
  • private

    • 私有函数和变量只在它们所定义的合约中可用(不在其他合约中:派生或次生)。Private是Internal的一个子集。
    • 它们只能通过getter函数在当前合约中被访问。
  • internal

    • 函数只能从它们所定义的合约中和派生合约中访问。EOA是无法访问的
    • 对于状态变量来说,默认是internal的,外部无法访问,但它们可以在当前合约和派生合约中被访问。
  • external

    • 该函数是合约接口的一部分,只能其他合约使用交易的EOA账户访问。它也是public的。

    • External不适用于状态变量

现在我们来看一些例子:

在这里我们定义了两个状态变量,x是public的,y是默认internal的

  • get_y函数,是public的可以返回成员变量 y
  • f1函数,是private的,只能在合约A中调用,其他账户一概不能调用
  • f2函数,是public的,但是在public函数中可以调用private函数,因为这是在合约A里面
  • f3函数,是internal的,可以在合约A中调用,也可以在合约A的派生合约中调用。但其他账户不能调用
  • f4函数,是external的,只能在外部调用,合约A内部无法调用f4,同样也无法在合约A的派生合约中调用f4
  • B合约,这里使用了类似于继承的思想,用B is A 表示继承,在B合约中,可以调用f2和f3,f1和f4是无法被调用的。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30

contract A {
int public x = 10;
int y = 20;

function get_y() public view returns (int){
return y;
}

function f1() private view returns(int){
return x;
}

function f2() public view returns(int){
int a;
a = f1();
return a;
}

function f3() internal view returns(int){
return x;
}

function f4() external view returns(int){
return x;
}
}
contract B is A{
int public xx = f3();
}

我们部署以后,发现在合约外,是无法调用f1、f3的。

最后,我们试一下在合约里面部署一个合约,然后调用内部合约的函数。在合约C中我们创建了一个新的A类型的合约。我们发现,可以调用external函数(f4)和public函数(f2),但是无法调用private函数(f1)和internal函数(f3)

因此我们知道,就算在合约中创建的新合约,也是无法调用新合约中的private和internal函数的

1
2
3
4
5
6
7
contract C{
A public contract_a = new A();
int public xx = contract_a.f4();
int public xxx = contract_a.f2();
// int public y = contract_a.f1();
// int public yy = contract_a.f3();
}
-------------本文结束,感谢您的阅读-------------