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 | // SPDX-License-Identifier: GPL-3.0 |
在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 | [ |
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测试网络一开始也是没有以太币的,我们需要到某些特殊的网站中去乞讨(水龙头网站),比如:
- https://faucet.egorfine.com/ ,每次可以领0.15个以太币,间隔时长为1天,只限制Ropsten测试网络
- https://faucet.dimensions.network/ ,每次可以领取1个以太币,间隔时长为1天,Ropsten测试网络
- https://faucets.chain.link/rinkeby ,每次可以领0.1个以太币,不限次数,限制在Kovan和Rinkeby测试网络
- https://moonborrow.com/ , 每次随机,仅限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的概念,我们以这个合约为例,看看合约里面有哪些成员
- 首先是两个成员变量 price和owner
- 然后是一个构造函数,构造函数只会被调用一次
- 接下来是一个Function Modifier,也就是函数修饰器。利用函数修饰器可在执行函数之前自动检查条件。比如说,这边changeOwner就调用了这个函数修饰器。是用来检查发起者是否等于当前owner的,如果不是,就会抛出错误信息。
- 在定义onlyOwner出现的特殊符号
_
,是使用该修饰器的函数体插入位置。 ‘_’符号可多次出现,替换成对应的函数体即可。
- 在定义onlyOwner出现的特殊符号
- changeOwner函数和setPrice都是 setter,即给合约的成员变量赋值的。
- getPrice函数时getter,用来返回price,因此需要在定义函数后还要写上返回值的类型。
以上函数都是public的,也就是外部可以调用的(比如python可以就可以通过这些函数调用来获取一些值)
最后是event(事件),事件是以太坊虚拟机(EVM)日志基础设施提供的一个便利接口。当被发送事件(调用)时,会触发参数存储到交易的日志中(一种区块链上的特殊数据结构)。这些日志与合约的地址关联,并记录到区块链中.
1 | // SPDX-License-Identifier: GPL-3.0 |
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(本地变量)两种。
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;
}
- ```solidity
view & pure
在这里还要区分一下两个函数修饰符:view和pure,如上面我们就用pure来修饰f1
- View 表示一个函数不能修改状态,在本地执行时并不消耗gas
- 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 | function setPrice(int _price) public{ |
getter
- getter: getter需要向调用者返回信息,因此不需要传参。由于getter只需要访问状态变量,不需要改写状态变量,因此需要用view来进行修饰。同时因为要返回信息,因此在view修饰词之后还要写
return(返回信息的类型)
1 | function getPrice() public view returns(int){ |
The Constructor
构造函数我们已经很熟悉了,当合约被创建的时候它就会被调用,且仅被调用一次。默认是public的,我们不用显式声明。
我们看到下面这个构造函数中,出现了owner = msg.sender
,这是什么意思
- msg.sender,它指的是当前调用者(或智能合约)的 address
- 在 Solidity 中,功能执行始终需要从外部调用者开始。 一个合约只会在区块链上什么也不做,除非有人调用其中的函数。所以 msg.sender总是存在的。
在这里,由于是我自己发布的合约,因此这里msg.sender就是我的address
1 | contract Property{ |
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递增,支持从 uint8
到 uint256
,以及 int8
到 int256。需要注意
的是,uint
和 int
默认代表的是 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
15contract 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 | contract FixedSizeArrays{ |
当我们访问越界的时候,就会报错
变长数组 Dynamically-Sized Arrays
变长数组顾名思义可以改变是数组的长度, 类似于一个栈,后进先出。
成员变量
.length
表示这个字符数组的长度
.push
表示往这个数组中添加成员
.pop
表示将数组中某成员删除
1 | contract DynamicArrays{ |
我们也可以定义局部变量数组。 需要注意的是,array属于引用类型,需要显式用memory修饰。和Storage Array不同,Memory Array无法使用pop和push,而且也无法使用.length得到长度。因此,如果要将Memory Array赋值给Storage Array的话,需要在声明时注明数组的长度。
1 | function f() public { |
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 | contract DynamicArrays{ |
Structs and Enums
- Struct
Solidity中的Struct与C语言中的struct类似。是一个键值对的集合,类似于映射,但值可以有不同的类型。
struct 会被存储在storage里面。一般来说,struct是定义在contract外的。下面我定义了一个Instructor 结构,然后在智能合约中通过constructor进行初始化。
1 | struct Instructor { |
- Enum
Enum和java中的Enum类似,可以用于定义取值范围有限的类型。Solidity 中Enum可以和整形显式的相互转换,整形再转换成enum时,编译器/EVM会检查取值范围,如果范围有误则会产生一个错误。 下面是一个例子:
1 | contract EnumTest { |
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 | contract Auction{ |
Overflows and Underflows
在早期的solidity版本中,整型变量是会发生上溢和下溢的,比如,我把solidity版本置为0.5.0, 然后定义一个uint8=255的变量,利用一个函数调用让其发生上溢:
1 | pragma solidity ^0.5.0; |
如上所示,我若将uint8类型的255加上1,不会报错,但是数值从255变为了0
在比较新的solidity版本中(比如0.8.0),当我使用同样的代码时,调用该函数会报错,x保持255的值不变。这是因为在新版本的solidity对可运算的数值类型加入了自动检查的功能。如果我们偏要忽略运算检查,可以使用unchecked来修饰,比如:
1 | function f1() public{ |
此外,还有一个显著的差别就是:bytes变量可以调用push函数插入字符、可以用pop弹出字符、可以用length获取长度,但是string都不行。
1 | contract DynamicArrays{ |
需要注明的是,相比于变长数组,定长数组消耗更少的gas,因此能用则用。
Built-In Global Variables
现在我们要学习一些solidity 内建的全局变量。其实之前我们已经接触到了这些变量,比如msg系列的、block系列的
abi
abi.encode(...) returns (bytes)
:对给定的参数进行ABI编码。abi.encodePacked(...) returns (bytes)
: Performes packed encoding of the given argumentsabi.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
): 当前块的gaslimitblock.number
(uint
):当前块的数量block.timestamp
(uint
):当前块的时间戳,注:不要依赖于block.timestamp
,now
和blockhash
用作随机性的来源,除非你知道你在做什么。
1 | contract Academy{ |
在这里,由于是在JS VM上跑的,所以block_number并不是真实的。
msg系列
msg.data
(bytes
): 完整的calldatamsg.sender
(address
): 消息的发送者(当前调用)msg.value
(uint
): 和消息一起发送的wei的数量- ```solidity
function sendEther() public payable{
}sendValue = msg.value;
- ```solidity
其他
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
}
- ```solidity
<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()
: 是一种最安全的方法用于发送ETHsend()
:类似于低配版的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会被调用 - 如果两个函数都没有,这个合约就没法通过常规的转账交易接收以太币(会抛出异常).
- 一个合约最多有一个
- fallback函数的性质如下:
- 调用payable function,并在那个交易中附带着发送ETH
1 | contract Deposit{ |
为了调试这个合约,我们需要将其在以太坊测试网络上进行部署:上链之后,我们看到这个合约地址已经生成了。
在Remix上,我们可以调用getBalance来看合约账户的余额,由于没有为其转账,因此余额为零
方式1: 外部账户给合约账户直接转账
现在,我到 MEW 网站上又注册了一个账号,并向这个合约地址转账了0.2个ETH,然后,我们就可以看到
调用合约内部的payable function
当然,我们也可以采取第二种方式调用合约内部的 payable function来对合约进行转账。
首先我们要创建一个payable function:
1 | function sendEther() public payable{ |
然后,我们需要在value中设置需要转账的钱, 1Finney = 0.001ETH
点击sendEther按钮之后,就会转接到metamask界面进行转账操作了。
Accessing the Contract’s Balance
前面我们学习了如何向一个合约转账,现在我们要来学如何获取一个合约账户的余额,并向其它合约账户、外部账户转账。
为此,我们要设计一个transfer函数用来实现账户之间的转账。
1 | function transferEther(address payable recipient,uint amount)public returns(bool){ |
部署以后我们往这个账户中转一些以太币以供后续测试用。然后,我们要给上面的那个合约转账的话,我们只需在transferEther里面输入目标地址和转账数目即可。
值得注意的是,这仅仅涉及合约与合约之间的转账,但是转账交易需要消耗一定的燃料,这仍然是需要EOA来支付的。
Protecting the Contract’s Balance
现在有一个问题,就是说我们这个transferEther函数时完全公开的,外部的人只要能获取合约地址就可以调用这个函数,因此是非常不安全的。因此对一个合约我们要保护好其余额.
为此我们可以使用 require 函数,它可以在执行函数逻辑之前做一些检查。这里我们首先创建一个owner状态变量,然后为其创建构造函数并为其赋值。因此,当这个合约被部署的时候,owner就会被设定为部署者的账号。然后,当要调用transferEther函数的时候,会调用require(owner == msg.sender)
进行核查,如果调用者不是owner,那么就会直接返回false。
1 | contract Deposit{ |
我们可以来做个实验, 我们用账户1部署好合约之后,切换到账户2,然后进行转账活动
Variables and Functions Visibility Private, Public, Internal, External
函数类型分为internal
和external
, 标记有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 |
|
我们部署以后,发现在合约外,是无法调用f1、f3的。
最后,我们试一下在合约里面部署一个合约,然后调用内部合约的函数。在合约C中我们创建了一个新的A类型的合约。我们发现,可以调用external函数(f4)和public函数(f2),但是无法调用private函数(f1)和internal函数(f3)
因此我们知道,就算在合约中创建的新合约,也是无法调用新合约中的private和internal函数的
1 | contract C{ |