区块链入门(一) —— Solidity

Solidity 源文件布局

pragm(版本杂注)

源文件可以被版本杂著pragma所注解,表明要求的编译器版本,例如:

1
pragma solidity ^0.4.0;

在上述限定下,源文件将不允许低于0.4.0版本的编译器编译,也不允许高于0.5.0版本的编译器编译

import

solidity支持使用import导入其他源文件(语法与JavaScript类似),主要有以下几种:

1
2
3
4
5
6
7
8
9
10
11
// 从"filename"中导入所有的全局符号到当前全局作用域中
import "filename"

// 创建一个新的全局符号 symbolName,其成员均来自"filename"中全局符号
import * as symbolName from "filenam"

// 创建新的全局符号 alias 和 symbol2,分别从 "filename"引用 symbol1 和 symbol2
import {symbol1 as alias, symbol2} from "filename"

// 这条语句等同于 import * as symbolName from "filename"
import "filename" as symbolName;

Solidity 值类型

类型 说明
布尔(bool) 可能的取值为字符串常量值true或false
整型(int/uint) 分别表示有符号和无符号的不同位数的整型变量;支持关键字uint8到uint256(无符号,从第8位到第256位)以及int8到int256,以8位为步长递增
定长浮点型(fixed / ufixed) 表示各种大小的有符号和无符号的定长浮点型;在关键字unfixedMxN和fixedMxN中,M表示该类型占用的位数,N表示可用的小数位数(fixed是ufixed128x19的别名)
地址(address) 存储一个20字节(160位)的值(以太坊地址大小)
定长字节数组 关键字有bytes1, bytes2, … ,bytes32
枚举(enum) 一种用户可以定义类型的方法,与C语言类似,默认从0开始递增,一般用来模拟合约的状态
函数(function) 一种表示函数的类型

Solidity 引用类型

Solidity中较难理解的是引用类型,主要有三种:数组(Array)、结构(Struct)、映射(Mapping)

数组(Array)

  • 数组可以在声明时指定长度(定长数组),也可以动态调整大小(变长数组、动态数组)
  • 对于存储型(storage)的数组 来说,元素类型可以是任意的(即元素也可以是数组类型、映射类型或者结构体);对于内存型(memory)的数组来说,元素类型不能是映射(mapping)的类型

结构(Struct)

  • Solidit支持通过构造结构体定义新的类型(与C语言的类似)

映射(Mapping)

  • 映射可以视作哈希表,在实际的初始化过程中创建的每个可能的key,并将其映射到字节形式全是零的值(类型默认值)

Solidity 地址类型

版本更新后的地址类型

address

  • 地址类型存储一个20字节的值(以太坊地址的大小);地址类型也有成员变量,并作为所有合约的基础

address payable(v0.5.0引入)

  • 与地址类型基本相同,不过多出了transfer和send两个成员变量

两者的转换和区别

  • payable地址是可以发送ether的地址,而普通address不能
  • 允许从payable address到address的隐式转换,而反过来的转换是不可能的(唯一方法是通过uint160进行中间转换)
  • 从0.5.0版本起,合约不再是从地址类型派生而来,但如果它有payable的回退函数,则同样可以显示转换为address或者address payable类型

我们可以看到,Solidity目前的发展趋势是越来越严格限制对地址合约交易的运用,这正是出于安全性的考量。

地址类型成员变量

  • 获得该地址的ether余额,以Wei为单位:
1
<address>.balance(uint256)
  • 向指定地址发送数量为amount的ether(以Wei为单位),失败时抛出异常,发送2300gas的矿工费,不可调节
1
<address payable>.transfer(uint256 amount)
  • 向指定地址发送数量为amount的ether(以Wei为单位),失败时返回false,发送2300gas的矿工费,不可调节(默认是执行成功的,更推荐使用transfer)
1
<address payable>.send(uint256 amount) returns(bool)
  • 发出底层函数CALL,失败时返回 false,发送所有可用gas,可调节(注意:如果CALL了别人的函数,这一段逻辑的控制权全部放在这个函数中,你不知道这个函数会做什么事情,可能把你的逻辑全部搞乱。。谨慎使用)
1
<address>.call(bytes memory) returns(bool, bytes memory)
  • 发出底层函数DELEGATECALL,失败时返回 false,发送所有可用gas,可调节
1
<address>.delegatecall(bytes memory) returns(bool, bytes memory)
  • 发出底层函数STATICCALL,失败时返回 false,发送所有可用gas,可调节
1
<address>.staticcall(bytes memory) returns(bool, bytes memory)

地址成员变量用法

  • balance和transfer
    • 可以使用balance属性来查询一个地址的余额,可以使用transfer函数像一个payable地址发送以太币(Ether),以Wei为单位:
1
2
3
4
address payable x = address(0x123)
address myAddress = address(this)
if(x.balance < 10 && myAddress.balance >= 10)
x.transfer(10); //给x发10Wei
  • send

    • send事transfer的低级版本,如果执行失败,当前的合约不会因为异常而终止,但send会返回false
  • call

    • 也可以用call来实现转币的操作,通过添加.gas()和.value()修饰器(很底层)
1
nameReg.call.gas(1000000).value(1 ether)(abi.encodeWithSignature("refister(string)", "MyName"));

类型详解

字符数组(Bytes Arrays)

定长字符数组

  • 属于值类型,bytes1,bytes2,…,bytes32分别代表了长度为1到32的字节序列
  • 有一个.length属性,返回数组长度(只读)

变长字符数组

  • 属于引用类型,包括bytes和string,不同的事bytes事Hex字符串,而string是UTF-8编码的字符串

枚举

  • 枚举类型用于用户自定义一组常量值
  • 与C语言的枚举类型非常相似,对应整型值
1
2
3
4
pragma solidity >=0.4.0 <0.6.0;
contract Purchase{
enum State{Create, Locked, Inactive}
}

数组(Array)

  • 固定大小k和元素类型T的数组被写为T[k],动态大小的数组为T[]。例如,一个由5个uint的动态数组组成的数组是uint[][5](定义的方式与C相反)
  • 要访问第三个动态数组中的第二个uint,可以使用x[2][1]
  • 越界访问数组,会导致调用失败回退
  • 如果要添加新元素,则必须使用.push()或将.length增大
  • 变长的storage数组和bytes(不包括string)有一个push()方法。可以将一个新元素附加到数组末端,返回值为当前长度

数组实例

1
2
3
4
5
6
7
8
9
10
11
12
pragma solidity >=0.4.16 <0.6.0;
contract{
function f(uint len) public pure{
// 给a分配了7个对应的存储空间
uint[] memory a = new uint[](7);
// 动态大小数组
bytes memory b = new bytes(len);
assert(a.length == 7);
assert(b.length == len);
a[6] = 8;
}
}

结构(Struct)

  • 结构类型可以在映射和数组中使用,它们本身可以包含映射和数组
  • 结构不能包含自己类型的成员,但可以作为自己数组的成员,也可以作为自己映射成员的值类型

结构实例

1
2
3
4
5
6
7
8
pragma solidity >=0.4.0 <0.6.0;
contract Ballot{
struct Voter {
uint weight;
bool voted;
uint vote;
}
}

映射(Mapping)

  • 声明一个映射:mapping(_KeyType => _ValueType)
  • _KeyType可以是任何基本类型,这意味着它可以是任何内置值类型加上字节和字符串,不允许使用用户自定义的或是更复杂的类型,如枚举,映射,结构以及除了bytes和string之外的所有数组类型
  • _ValueType可以是任何类型,包括映射

映射实例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
pragma solidity >=0.4.0 <0.6.0;
contract MappingExample {
mapping(address =>uint) public balances;
function update(uint newBalance) public {
balances[msg.sender] = newBalance;
}
}

contract MappingUser{
function f() public returns (uint) {
MappingExample m = new MappingExample();
m.update(100);
return m.balances(address(this));
}
}

Solidity 数据存储位置

  • 所有的复杂类型,即数组、结构和映射类型,都有一个额外属性——”数据位置”,用来说明数据时 保存在内存(memory)中还是存储(storagez)中
  • 根据上下文不同,大多数时候数据有默认的位置,但也可以通过在类型名后增加关键字 storage或memory进行修改
  • 函数参数(包括返回的参数)的数据位置默认是memory,局部变量的数据位置默认是storage,状态变量的数据位置强制是storage
  • 另外还存在第三种 数据位置,calldata,这是一块只读的且不会永久存储的位置,用来存储函数参数,外部函数的参数(非返回参数)的数据位置被强制指定为calldata,效果跟memory差不多

总结

  • 强制指定地数据位置
    • 外部函数的参数(不包括返回参数):calldata
    • 状态变量:starage
  • 默认数据位置
    • 函数参数(包括返回参数):memory
    • 引用类型地局部变量:storage
    • 值类型地局部变量:栈(stack)

特别要求

  • 公开可见(public visible)的函数参数一定是memory类型,如果要求是storage类型,则必须是private或者internal参数,这是为了防止随意的公开调用占用资源

数据存储实例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
pragma solidity ^0.4.0;
contract C{
uint[] data1;
uint[] data2;
function appendOne() public {
append(data1);
}
function appendTwo() public {
append(data2);
}
function append(uint[] storage d) internal {
d.push(1);
}
}

改错题

#T1

1
2
3
4
5
6
7
8
9
10
pragma solidity ^0.4.0;
contract C{
uint someVariable;
uint[] data;
function f() public{
uint[] x;
x.push(2);
data = x;
}
}

someVariable在这段代码里面会成为一个计数器,每一次调用f()都会使之递增,这是由于Solidity语法中,未指定的x在这里被设定为一个storage类型的指针(storage pointer),由于所有状态变量和局部变量的默认存储位置都是在storage中,因此声明了一个可变长度的数组x又未给它赋值,因此在存储空间中它是一个没有分配存储空间的指针,最开始一轮a和b都赋初值0,而x会指向合约定义的整个存储空间的零位置(最开始的地方),也就是a的位置,因此后面在调用x之后a会随之变化。

但是为什么x变化2,而a只变化了1呢?因为我们定义的x这个长度可变的数组在顺序存储中会存储它的长度,找这个变量的时候就会直接找到它的长度,我们想要找到它的元素就会通过它元素的索引值加上本身的位置共同计算出一个哈希,哈希的位置就是元素对应的位置。

因此正确方式为:

1
2
3
4
5
6
7
8
9
pragma solidity ^0.4.0;
contract C{
uint someVariable;
uint[] data;
function f() public{
uint[] x = data;
x.push(2);
}
}

#T2

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
pragma solidity ^0.4.0;
contract C{
uint[] x;
function f(uint[] memoryArray) public {
x = memoryArray;
uint[] y = x;
y[7];
y.length = 2;
delete x;
y = memoryArray;
delete y;
g(x);
h(x);
}
function g(uint[] storage storageArray) internal {}
function h(uint[] memoryArray) public {}
}

上面这段代码在y = memoryArray处会报错:Type uint256[] memory is not implicitly convertible to expected type uint256[] storage pointer.显见,memoryArray为一个memory类型的变长数组,而要将之赋值给y,由于y是一个storage类型的指针,不会发生拷贝,产生错误。(两块空间的地址意义不同)

因此正确方式为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
pragma solidity ^0.4.0;
contract C{
uint[] x;
function f(uint[] memoryArray) public {
x = memoryArray;
uint[] y = x;
y[7];
y.length = 2;
delete x;
uint[]memory z = memoryArray;
delete z;
g(x);
h(x);
}
function g(uint[] storage storageArray) internal {}
function h(uint[] memoryArray) public {}
}

#T3 猜数字游戏

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
pragma solidity >0.4.22;
contract Honeypot {
uint luckyNum = 52;
uint public last;
struct Guess{
address player;
uint number;
}
Guess[] public guessHistory;
address owner = msg.sender;
function guess(uint_num) public payable{
Guess newGuess;
newGuess.player = msg.sender;
newGuess.number = _num;
guessHistory.push(newGuess);
if(_num == luckyNum)
msg.sender.transfer(msg.value*2);
last = now;
}
}

当传入guess的参数52时,发现账户余额净少十个,并没有出现返还的两倍奖励。首先Guess newGuess和T1一样,被声明为一个storage类型的指针,也就是最初会直接指向luckyNum,并将之修改。这是Solidity上的一个钓鱼合约(蜜罐合约),发多少以太币丢多少以太币。另外事实上若将Guess中新得到的luckyNumber输进去是可以成功的,可以将币提出来,但实际上合约创作者不会愚蠢地把luckyNum设置为public。

我们想一下,若将Guess定义中的player和number互换一下次序会发生什么:由于Guess会首先传入调用者写入的内容,也就是number,因此调用者无论输入了 什么数字最终都会显示猜测正确,于是给调用者转钱。

不过上述问题在0.5.0版本之后已被更正,但以太坊上仍有不少钓鱼合约需要我们注意。

函数详解

函数声明和类型

在下面的函数声明中:

1
2
3
function getBrand() public view returns (string){
return brand;
}

getBrand()时函数名称,public view是函数类型,string是返回类型。

  • 函数的值类型有两类:内部函数和外部函数
  • 内部函数只能在当前合约内被调用(更具体来说,在当前代码块内,包括内部库函数和继承的函数中),因为它们不能再当前合约上下文的外部被执行。调用一个内部函数是通过跳转到它的入口标签来实现的,就像在当前合约的内部 调用一个函数
  • 外部函数由一个地址和一个函数签名组长城,可以通过外部函数调用传递或者返回
  • 调用内部函数:直接使用名字f
  • 调用外部函数: this.f(当前合约),a.f(外部合约)

函数可见性

函数的可见性可以指定为external,public,internal或者private;对于状态变量,不能设置为external,默认是internal。

  • external:外部函数作为合约接口的一部分,意味着我们可以从其他他合约和交易中调用。一个挖补函数f不能从内部调用(即f不起作用,但this.f()可以)。当收到大量数据时,外部函数有时会更有效率。
  • public:public函数是合约接口的一部分,可以在内部或通过消息调用。对于public状态变量,会自动生成一个getter函数。
  • internal:这些函数和状态变量只能 是内部访问(即从当前合约内部或从它的派生的合约访问),不能用this调用。
  • private:private函数和状态变量仅在当前按定义 它们的合约中使用,并且不能被派生合约使用。

我们在将一个状态变量设置为public时,实际上就是生成一个getter函数:

1
2
3
function a() public view returns(uint){
return a;
}

函数状态可变性

  • pure:纯函数,不允许修改或访问状态
  • view:不允许修改状态
  • payable:允许从消息调用中接收以太币
  • constant:与view相同,一般只修饰状态变量,不允许赋值(只能初始化)
  • 以下情况被认为是修改状态:
    • 修改状态变量
    • 产生事件
    • 创建其他合约
    • 使用selfdestruct
    • 通过调用发送以太币
    • 通过任何没有标记为view或者pure的函数
    • 使用低级调用(CALL等)
    • 使用包含特定操作码的内联汇编
  • 以下被认为是读取状态:
    • 读取状态变量
    • 访问this.balance胡总和
      .balance
    • 访问block,tx,msg中任意成员(除msg.sig和msg.data之外)
    • 调用任何未标记为pure的函数
    • 使用包含某些操作码的内联汇编

函数修饰器(modifier)

  • 使用修饰器modifier可以轻松改变函数的行为,例如:它们可以在执行函数之前自动检查某个条件。修饰器modifier是合约的可继承属性,并可能被派生合约覆盖
  • 如果同一个函数有多个修饰器modifier,它们之间以空格隔开,修饰器modifier会依次检查执行

modifier实例

1
2
3
4
5
6
7
8
9
10
11
12
pragma solidity >=0.4.22 <0.6.0;
contract Purchase {
address public seller;
modifier onlySeller(){
requier(msg.sender == sender, "Only seller can sell.");
_; // 占位符:原本函数的代码在此执行
}
function abort public view onlySeller returns(uint){
// Modifier usage
return 200;
}
}

当换一个账户调用f时会报错,判断了require。同理函数修饰器也可以改变下划线 (占位符的位置),改变调用次序。

回退函数(fallback)

  • 回退函数(fallback function)是合约中的特殊函数,没有名字,不能有参数也不能有返回值
  • 如果在一个合约的调用中,没有其他函数与给定的函数标识符匹配(或没有提供调用数据),那么这个函数(fallback)函数会被执行
  • 每当合约收到以太币(没有任何数据),回退函数就会执行。此外,为了接收以太币,fallback必须标记为payable。如果不存在这样的函数,则合约不能通过常规交易接收以太币
  • 在上下文中通常只有很少的gas可以用来 完成回退函数的调用,所以使用fallback函数的调用尽量廉价很重要

回退函数中如果有人加入恶意代码,比如获取地址,比如重复调用合约,就会产生一个比较大的安全隐患。这种现象一般会发生在发币的合约中,恶意账户在回退函数中加入能重复调用合约的代码,从而达成不断获得币的目的。以太坊历史上最大的一次攻击——The Dao,正是由于以太坊在合约中给人转币,调用了transfer方法,但没有判定地址到底是什么样的地址,结果就是黑客写了份合约,注册了The Dao帐号之后触发了函数Transfer往自己合约中转以太,并且调用到了自己写好的回退函数,当然还使用到了其他漏洞大家可以自行了解。

事件

  • 事件是以太坊EVM提供的一种日志基础设施,事件可以用来做操作记录,存储为日志。也可以用来实现一些交互功能,比如同值UI,返回函数调用结果等
  • 当定义的事件触发时,我们可以将时间存储到EVM的交易日志中,日志是区块链中的一种特殊的数据结构,日志与合约关联,与合约的存储和并存入区块链中;只要某个区块可以访问,其相关的日志就可以访问;但在合约中我们不能直接访问日志和事件数据
  • 可以通过日志实现简单支付验证SPV(Simpllified Payment Verification),如果一个外部实体提供了一个带有这种证明的合约,它可以检查日志是否真实存在于区块链中

Solidity异常处理

  • Solidity使用“状态恢复异常”来处理异常。这样的异常将撤销 对当前调用(及其所有子调用)中的状态 所做的所有更改,并且向调用者返回错误
  • 函数assert和require可用于判断条件,并在不满足条件时抛出异常
  • assert()一般只应用于测试内部错误,并检查常量
  • require()应用于确保 满足有效条件(如输入或合约状态变量),或验证调用外部合约的返回值
  • revert()用于抛出异常,它可以标记一个错误并返回当前调用回退(检查外部)
  • 版权声明: 本博客所有文章除特别声明外,著作权归作者所有。转载请注明出处!

扫一扫,分享到微信

微信分享二维码
  • Copyrights © 2020-2021 chenk
  • 由 帅气的CK本尊 强力驱动
  • 访问人数: | 浏览次数:

请我喝杯咖啡吧~

支付宝
微信