草根之明

管理及技术博客

0%

solidity编程攻略之一

Solidity是一门面向合约的、为实现智能合约而创建的高级编程语言。这门语言受到了C++,Python和Javascript语言的影响,设计的目的是能在以太坊虚拟机(EVM)上运行。

智能合约文件结构

版本申明

1
pragma solidity ^0.4.0;

说明:
1 版本要高于0.4才可以编译
2 号表示高于0.5的版本则不可编译,第三位的版本号但可以变,留出来用做bug可以修复(如0.4.1的编译器有bug,可在0.4.2修复,现有合约不用改代码)。

引用其它源文件

  • 全局引入 *
1
import “filename”;
  • 自定义命名空间引入 *
1
import * as symbolName from “filename”

分别定义引入

1
import  {symbol1 as alias, symbol2} from “filename”

非es6兼容的简写语法

1
import “filename” as symbolName

等同于上述

1
import * as symbolName from “filename”

关于路径

引入文件路径时要注意,非.打头的路径会被认为是绝对路径,所以要引用同目录下的文件使用

1
import “./x” as x

也不要使用下述方式,这样会是在一个全局的目录下

1
import “x” as x;

为什么会有这个区别,是因为这取决于编译器,如果解析路径,通常来说目录层级结构并不与我们本地的文件一一对应,它非常有可能是通过ipfs,http,或git建立的一个网络上的虚拟目录。

编译器解析引用文件机制

各编译器提供了文件前缀映射机制。

  1. 可以将一个域名下的文件映射到本地,从而从本地的某个文件中读取
  2. 提供对同一实现的不同版本的支持(可能某版本的实现前后不兼容,需要区分)
  3. 如果前缀相同,取最长,
  4. 有一个”fallback-remapping”机制,空串会映射到“/usr/local/include/solidify”

solc编译器

命令行编译器,通过下述命令命名空间映射提供支持

1
context:prefix=target

上述的context:=target是可选的。所有context目录下的以prefix开头的会被替换为target
举例来说,如果你将github.com/ethereum/dapp-bin拷到本地的/usr/local/dapp-bin,并使用下述方式使用文件

1
import “github.com/ethereum/dapp-bin/library/iterable_mapping.sol” as it_mapping;

要编译这个文件,使用下述命令:

1
solc github.com/ethereum/dapp-bin=/usr/local/dapp-bin source.sol

另一个更复杂的例子,如果你使用一个更旧版本的dapp-bin,旧版本在/url/local/dapp-bin_old,那么,你可以使用下述命令编译

1
2
3
solc module1:github.com/ethereum/dapp-bin=/usr/local/dapp-bin  \
modeule2:github.com/ethereum/dapp-bin=/usr/local/dapp-bin_old \
source.sol

需要注意的是solc仅仅允许包含实际存在的文件。它必须存在于你重映射后目录里,或其子目录里。如果你想包含直接的绝对路径包含,那么可以将命名空间重映射为=\
备注:如果有多个重映射指向了同一个文件,那么取最长的那个文件。

代码注释

两种方式,单行(//),多行使用(/*…*/)

示例

1
2
3
4
5
// this is a single-line comment
/*
this is a
mulit-line comment
*/

文档注释

写文档用。三个斜杠////** … */,可使用Doxygen语法,以支持生成对文档的说明,参数验证的注解,或者是在用户调用这个函数时,弹出来的确认内容。

示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
pragma solidity ^0.4.0;
/** @title Shape calculator.*/
contract shapeCalculator{
/**
*@dev calculate a rectangle's suface and perimeter
*@param w width of the rectangles
*@param h height of the rectangles
*@return s surface of the rectangles
*@return p perimeter of the rectangles
*/

function rectangles(uint w, uint h) returns (uint s, uint p){
s = w * h;
p = 2 * ( w + h) ;
}
}

智能合约源文件基本要素概览(Structure of a Contract)

Solidity 合约和面向对象语言非常相似。每个合约均能包含状态变量State Variables, 函数Functions, 函数修饰符Function Modifiers, 事件Events, 结构体类型Struct Types 和 枚举类型Enum Types。除此以外,还有比较特殊的合约叫做库libraries和接口interfaces

  • 合约类似面向对象语言中的类。
  • 支持继承

状态变量 State Variables

状态变量是其永远存储在合约实例中的变量。

1
2
3
4
5
6
7
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.7.0 <0.9.0;

contract SimpleStorage {
uint storedData; // 状态变量
// ...
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.13;

contract Variables {
// 状态变量存储在区块链上
string public text = "Hello";
uint public num = 123;

function doSomething() public {
// 局部变量不保存到区块链
uint i = 456;

// 全局变量
uint timestamp = block.timestamp; // 当前块的时间戳
address sender = msg.sender; // 来访者地址
}
}

详情见类型(Types)章节,关于所有支持的类型和变量相关的可见性(Visibility and Accessors)。

函数 Functions

函数是合约实例对象的一种行为,可以通过合约实例调用函数请求其帮助我们完成我们期望的某个任务。

函数是代码的可执行单元。函数通常在合约内定义,但也可以在合约外定义。

1
2
3
4
5
6
7
8
9
10
11
12
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.7.0 <0.9.0;
contract SimpleAuction {
function bid() public payable { // 函数、行为、方法
// ...
}
}

// Helper function defined outside of a contract
function helper(uint x) pure returns (uint) {
return x * 2;
}

函数调用可以设置为内部(Internal)的和外部(External)的。同时对于其它合同的不同级别的可见性和访问控制(Visibility and Accessors)。具体的情况详见后面类型中关于函数的章节。

函数修饰符 Modifier

修改器(modifier)可以用来轻易的改变一个函数的行为,控制函数的逻辑,比如用于在函数执行前检查某种前置条件。

修改器是一种合约属性,可以被继承,同时还可被派生的合约重写(override);

对于一个函数可以有多个修改器限制,在函数定义的时候依次写上,并用加空格分隔,执行的时候也是依次执行。多个修改器是同时限制,也就是说必须满足所有的修改器的权限,才可以执行函数体的代码;

1
2
3
4
5
modifier 函数修改器名(参数){
a++; // 代表函数前执行的代码
_; // 表示被修饰的函数中的代码
a--; // 代表函数后执行的代码

函数修饰符可用于以声明方式修改功能的语义。Modifier修饰符不支持重载,也就是相同的名字不可能出现两个。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.7.0 <0.9.0;
contract Purchase {
address public seller;
modifier onlySeller() { // Modifier修饰符
require(
msg.sender == seller,
"Only seller can call this."
);
}

// Modifier 使用场景,当调用`abort()`函数时,会先执行`onlySeller`检查是否满足条件,满足,继续执行,不满足,发生异常,停止
function abort() public view onlySeller {
// ...
}
}

事件 Events

事件是以太坊虚拟机(EVM)日志基础设施提供的一个便利接口。用于获取当前发生的事件。

1
2
3
4
5
6
7
8
9
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.7.0 <0.9.0;
contract SimpleAuction {
event HighestBidIncreased(address bidder, uint amount); // Event
function bid() public payable {
// ...
emit HighestBidIncreased(msg.sender, msg.value); // 触发 event
}
}

错误 Error

错误允许您为故障情况定义描述性名称和数据。错误可用于还原语句。与字符串描述相比,错误要便宜得多,并且允许您对附加数据进行编码。您可以使用 NatSpec 向用户描述错误。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// SPDX-License-Identifier: GPL-3.0
pragma solidity ^0.8.4;

/// Not enough funds for transfer. Requested `requested`,
/// but only `available` available.
error NotEnoughFunds(uint requested, uint available);

contract Token {
mapping(address => uint) balances;
function transfer(address to, uint amount) public {
uint balance = balances[msg.sender];
if (balance < amount)
revert NotEnoughFunds(amount, balance);
balances[msg.sender] -= amount;
balances[to] += amount;
// ...
}
}

结构体类型 Struct Types

结构是自定义的结构类型,可以对多个变量进行分组封装;

自定义的将几个变量组合在一起形成的类型。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.7.0 <0.9.0;
contract Ballot {
struct Voter { // Struct
uint weight;
bool voted;
address delegate;
uint vote;
}

struct manager{
Voter employ;
string title;
}
}

枚举 Enum Types

特殊的自定义类型,类型的所有值可枚举的情况。

1
2
3
4
5
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.7.0 <0.9.0;
contract Purchase {
enum State { Created, Locked, Inactive } // Enum
}

映射-mapping

映射是一种引用类型,存储键–值对

格式:mapping(key=>value)

在mapping中,key可以是整型、字符串等基本数据类型,但不能使用动态数组、contract、枚举、struct、以及mapping这些类型;

value的类型没有限制;

mapping不能作为参数的形参使用

库 library

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
35
36
37
38
39
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.7.0 <0.9.0;
struct Data {
mapping(uint256 => bool) flags;
}

library Set {
// 集合 Set library

function insert(Data storage self, uint256 value) public returns (bool) {
if (self.flags[value]) return false;
self.flags[value] = true;
return true;
}

function remove(Data storage self, uint256 value) public returns (bool) {
if (!self.flags[value]) return false;
self.flags[value] = false;
return true;
}

function contains(Data storage self, uint256 value)
public
view
returns (bool)
{
return self.flags[value];
}
}

contract C {
Data knownValues;

function register(uint256 value) public {
// library 函数不需要通过实例对象调用,可直接通过 library 名字直接调用
require(Set.insert(knownValues, value));
}
}

接口 Interface

接口类似于抽象协议,但是不能实现任何功能。还有其他限制:

  • 它们不能继承其他合约,但是可以从其他接口继承。
  • 所有声明的函数都必须是外部的。
  • 接口不能声明构造函数。
  • 接口不能声明状态变量。
1
2
3
4
5
6
7
8
9
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.7.0 <0.9.0;
interface ParentA {
function test() external returns (uint256);
}
interface ParentB {
function test() external returns (uint256);
}