Tags: Blockchain.
Categories: notes.

粗浅地学习了一下solidity

简单的智能合约

Solidity中智能合约的含义就是一组代码(它的 功能 )和数据(它的 状态)的集合,并且它们是位于以太坊区块链的一个特定地址上的

存储合约示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.4.16 <0.9.0; //声明版本范围

contract SimpleStorage {
uint storedData; //uint 256位无符号整数

function set(uint x) public {
storedData = x; //solidity中不需要用this.访问
}

function get() public view returns (uint) {
return storedData;
}
}

set:修改变量storeData的值

get:检索变量storeData的值

这个简单的合约允许任何人在合约中存储一个单独的数字,并且这个数字可以被任何人访问,且没有可行的方法阻止这个数字的发布

****货币合约(Subcurrency)示例****

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
// SPDX-License-Identifier: GPL-3.0pragma solidity ^0.8.4;

contract Coin {
// 关键字“public”让这些变量可以从外部读取
address public minter;
mapping (address => uint) public balances;

// 轻客户端可以通过事件针对变化作出高效的反应
event Sent(address from, address to, uint amount);

// 这是构造函数,只有当合约创建时运行
constructor() {
minter = msg.sender;
}

function mint(address receiver, uint amount) public {
require(msg.sender == minter);
balances[receiver] += amount;
}

// Errors allow you to provide information about
// why an operation failed. They are returned
// to the caller of the function.
error InsufficientBalance(uint requested, uint available);

function send(address receiver, uint amount) public {
if (amount > balances[msg.sender])
revert InsufficientBalance({
requested: amount,
available: balances[msg.sender]
});

balances[msg.sender] -= amount;
balances[receiver] += amount;
emit Sent(msg.sender, receiver, amount);
}
}

address public minter

address是个160位的值,且不允许任何算数操作。关键字public自动生成一个函数,允许在此合约之外访问这个状态变量的当前值。public会由编译器自动生成一段函数,代码大致如下(暂时忽略 external 和 view):

1
function minter() external view returns (address) { return minter; }

在使用public后再加上这段代码会造成冲突,会出现同名的一个函数和一个变量

mapping (address => uint) public balances

mapping类型,将address映射为无符号整数(uint)。在使用mapping时,要么记住你添加到mapping中的数据(使用列表或更高级的数据类型会更好),要么在不需要键列表或值列表的上下文中使用它。这里由public生成的函数要更加复杂一点,大致如下:

1
2
3
function balances(address account) external view returns (uint) {
return balances[account];
}

可以很轻松的查询账户的余额

event Sent(address from, address to, uint amount)

这行声明了一个所谓的“事件(event)”,它会在 send 函数的最后一行被发出。用户界面(当然也包括服务器应用程序)可以监听区块链上正在发送的事件,而不会花费太多成本。一旦它被发出,监听该事件的listener都将收到通知。而所有的事件都包含了 from , to 和 amount 三个参数,可方便追踪交易。

为了监听这个事件,你可以使用如下JavaScript代码, Coin 是通过 web3.js 创建的合约对象  :

1
2
3
4
5
6
7
8
9
10
Coin.Sent().watch({}, '',function(error, result) {
if (!error) {
console.log("Coin transfer: " + result.args.amount +
" coins were sent from " + result.args.from +
" to " + result.args.to + ".");
console.log("Balances now:\n" +
"Sender: " + Coin.balances.call(result.args.from) +
"Receiver: " + Coin.balances.call(result.args.to));
}
})

搞不懂event,菜的扣脚

constructor

创建合约期间运行的构造函数,不能在创建后调用。此合约中构造函数用来永久存储创建合约的人的地址msgmsg.sender始终记录当前(外部)函数调用是来自于哪一个地址

mint

mint函数用来新发行一定的币到一个新地址。require用来检查某些条件,不满足就会回推所有的状态变化。在这个合约中,require(msg.sender == minter)确保只有合约创建者可以调用mint。当balances[receiver] += amount的值大于uint(2**256-1),称为溢出交易,溢出交易将被还原

error

描述错误信息,一般和revert一起使用。revert语句无条件地中止执行并回退所有的变化,类似于 require函数,它也同样允许你提供一个错误的名称和额外的数据,这些额外数据将提供给调用者(并最终提供给前端应用程序或区块资源管理器)。error是一个专门用来返回错误,并且可以自定义返回内容的类型

send

进行交易的函数。此合约中先判断发送者的账户上是否有足够的余额,余额不足用revert语句中止并回退。余额足够则进行账户的交易。交易结束后,使用emit关键字触发事件

~~emitevnet都不是很懂~~

Knowledge point

状态变量、局部变量、全局变量

1
2
3
4
5
6
7
8
9
10
11
12
13
14
pragma solidity ^0.5.0;

contract SolidityTest {
uint storedData; // 状态变量
constructor() public {
storedData = 10;
}
function getResult() public view returns(uint){
uint a = 1; // 局部变量
uint b = 2;
uint result = a + b;
return result; // 访问局部变量
}
}
  • 状态变量:变量值永久保存在合约存储空间中的变量
  • 局部变量:变量值仅在函数执行过程中有效的变量,函数退出后,变量无效
  • 全局变量:全局工作区中存在的特殊变量,提供有关区块链和交易属性的信息
名称 返回
blockhash(uint blockNumber) returns (bytes32) 给定区块的哈希值 – 只适用于256最近区块, 不包含当前区块。
block.coinbase (address payable) 当前区块矿工的地址
block.difficulty (uint) 当前区块的难度
block.gaslimit (uint) 当前区块的gaslimit
block.number (uint) 当前区块的number
block.timestamp (uint) 当前区块的时间戳,为unix纪元以来的秒
gasleft() returns (uint256) 剩余 gas
msg.data (bytes calldata) 完成 calldata
msg.sender (address payable) 消息发送者 (当前 caller)
msg.sig (bytes4) calldata的前四个字节 (function identifier)
msg.value (uint) 当前消息的wei值
now (uint) 当前块的时间戳
tx.gasprice (uint) 交易的gas价格
tx.origin (address payable) 交易的发送方

变量作用域

  • Public – 公共状态变量可以在内部访问,也可以通过消息访问。对于公共状态变量,将生成一个自动getter函数。
  • Internal – 内部状态变量只能从当前合约或其派生合约内访问。
  • Private – 私有状态变量只能从当前合约内部访问,派生合约内不能访问。
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
pragma solidity ^0.5.0;

contract C {
uint public data = 30;
uint internal iData= 10;

function x() public returns (uint) {
data = 3; // 内部访问
return data;
}
}
contract Caller {
C c = new C();
function f() public view returns (uint) {
return c.data(); // 外部访问
}
}
contract D is C {
uint storedData; // 状态变量

function y() public returns (uint) {
iData = 3; // 派生合约内部访问
return iData;
}
function getResult() public view returns(uint){
uint a = 1; // 局部变量
uint b = 2;
uint result = a + b;
return storedData; // 访问状态变量
}
}

条件运算符

?::如果条件为真?则取值X:否则值Y

循环语句

与c类似

条件语句

与c类似

引用类型/复合数据类型

  • 数组(字符串与bytes是特殊的数组,所以也是引用类型)
  • struct(结构体)
  • map(映射)

这些类型涉及到的数据量较大,复制它们可能要消耗大量Gas,非常昂贵,所以使用它们时,必须考虑存储位置,例如,是保存在内存中,还是在EVM存储区中

数据位置

Storage(存储)

存储永久数据,该数据可以被合约中所有函数访问。可以把它视为硬盘数据,所有数据都永久存储。因其数据存储的持久性,故成本较高

Memory(内存)

临时数据,比Storage便宜。它只能在函数中访问,函数执行完毕,它的内容就会被丢弃

Calldata

不可修改的非持久性数据位置,所有传递给函数的值,都存储在这里。Calladata也是外部函数的参数的默认位置

Stack(堆栈)

EVM维护的非持久性数据,EVM使用堆栈数据位置在执行期间加载变量。最多有1024个级别的限制

变量数据位置规则

  • 状态变量:存储在存储区中。此外,不能显式标记状态变量的位置
  • 函数参数与返回值:存储在内存中
  • 局部变量:值类型存储在内存中如uint,bool,address,引用类型数组,struct,map需要显式地指定数据位置
  • 外部函数的参数:存储在Calldata中

枚举(Enum)

枚举将一个变量的取值限制为几个预定义值中的一个。精确使用枚举类型有助于减少代码中的bug

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

pragma solidity ^0.8.0;

contract test {
enum FreshJuiceSize{ SMALL, MEDIUM, LARGE }
FreshJuiceSize choice;
FreshJuiceSize constant defaultChoice = FreshJuiceSize.MEDIUM;

function setLarge() public {
choice = FreshJuiceSize.LARGE;
}
function getChoice() public view returns (FreshJuiceSize) {
return choice;
}
function getDefaultChoice() public pure returns (uint) {
return uint(defaultChoice);
}
}

结构体(Struct)

与C类似

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.5.0;

contract test {
struct Book {
string title;
string author;
uint book_id;
}
Book book;

function setBook() public {
book = Book('Learn Java', 'TP', 1);
}
function getBookId() public view returns (uint) {
return book.book_id;
}
}

映射(mapping)

1
mapping(_KeyType => _ValueType)
  • _KeyType :可以是任何内置类型,或者bytes和字符串。不允许使用引用类型或复杂对象
  • _ValueType : 可以是任何类型

注意

  • 映射的数据位置(data location)只能是storage,通常用于状态变量
  • 映射可以标记为public,Solidity 自动为它创建getter
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
pragma solidity ^0.5.0;

contract LedgerBalance {
mapping(address => uint) public balances;

function updateBalance(uint newBalance) public {
balances[msg.sender] = newBalance;
}
}
contract Updater {
function updateBalance() public returns (uint) {
LedgerBalance ledgerBalance = new LedgerBalance();
ledgerBalance.updateBalance(10);
return ledgerBalance.balances(address(this));
}
}

对示例代码写一点自己的理解:

address是不可被运算的数据类型,因此需要map映射为uint,再参与运算

类型转换

可以使用构造函数语法,显式地将数据类型转换为另一种类型。

1
2
3
int8 y = -3;
uint x = uint(y);
//Now x = 0xfffff..fd == 在256bit长度的格式下,-3的补码表示

转换成更小的类型,会丢失高位。

1
2
uint32 a = 0x12345678;
uint16 b = uint16(a); // b = 0x5678

转换成更大的类型,将向左侧添加填充位。

1
2
uint16 a = 0x1234;
uint32 b = uint32(a); // b = 0x00001234

转换到更小的字节类型,会丢失后面数据。

1
2
bytes2 a = 0x1234;
bytes1 b = bytes1(a); // b = 0x12

转换为更大的字节类型时,向右添加填充位。

1
2
bytes2 a = 0x1234;
bytes4 b = bytes4(a); // b = 0x12340000

只有当字节类型和int类型大小相同时,才可以进行转换。

1
2
3
4
5
bytes2 a = 0x1234;
uint32 b = uint16(a); // b = 0x00001234
uint32 c = uint32(bytes4(a)); // c = 0x12340000
uint8 d = uint8(uint16(a)); // d = 0x34
uint8 e = uint8(bytes1(a)); // e = 0x12

把整数赋值给整型时,不能超出范围,发生截断,否则会报错。

1
2
3
uint8 a = 12; // no error
uint32 b = 1234; // no error
uint16 c = 0x123456; // error, 有截断,变为 0x3456

以太单位

Solidity 中,以太币的单位可以使用weifinneyszaboether表示。

最小的单位是wei1e12表示1 x 10^12

1
2
3
4
assert(1 wei == 1);
assert(1 szabo == 1e12);
assert(1 finney == 1e15);
assert(1 ether == 1e18);

函数

语法:

1
2
3
function function-name(parameter-list) scope returns() {
//语句
}

return语句可以返回多个值

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

contract Test {
function getResult() public view returns(uint product, uint sum){
uint a = 1; // 局部变量
uint b = 2;
product = a * b; // 使用返回参数返回值
sum = a + b; // 使用返回参数返回值

// 也可以使用return返回多个值
// return(a*b, a+b);
}
}

修饰符:

函数修饰符用于修改函数的行为。例如,向函数添加条件限制。

修饰符定义中出现特殊符号_ 的地方,用于插入函数体。如果在调用此函数时,满足了修饰符的条件,则执行该函数,否则将抛出异常。

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
pragma solidity ^0.5.0;

contract Owner {
address owner;

constructor() public {
owner = msg.sender;
}

// 定义修饰符 onlyOwner 不带参数
modifier onlyOwner {
require(msg.sender == owner);
_;
}

// 定义修饰符 costs 带参数
modifier costs(uint price) {
if (msg.value >= price) {
_;
}
}
}

contract Register is Owner {
mapping (address => bool) registeredAddresses;
uint price;

constructor(uint initialPrice) public { price = initialPrice; }

// 使用修饰符 costs
function register() public payable costs(price) {
registeredAddresses[msg.sender] = true;
}

// 使用修饰符 onlyOwner
function changePrice(uint _price) public onlyOwner {
price = _price;
}
}

其中onlyOwnercosts是修饰符

view(视图)函数:

View(视图)函数不会修改状态。如果函数中存在以下语句,则被视为修改状态,编译器将抛出警告。

  • 修改状态变量。
  • 触发事件。
  • 创建合约。
  • 使用selfdestruct
  • 发送以太。
  • 调用任何不是视图函数或纯函数的函数
  • 使用底层调用
  • 使用包含某些操作码的内联程序集。

Getter方法是默认的视图函数。声明视图函数,可以在函数声明里,添加view关键字。

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

contract Test {
function getResult() public view returns(uint product, uint sum){
uint a = 1; // 局部变量
uint b = 2;
product = a * b;
sum = a + b;
}
}

pure(纯)函数:

Pure(纯)函数不读取或修改状态。如果函数中存在以下语句,则被视为读取状态,编译器将抛出警告。

  • 读取状态变量。
  • 访问 address(this).balance 或 <address>.balance
  • 访问任何区块、交易、msg等特殊变量(msg.sig 与 msg.data 允许读取)。
  • 调用任何不是纯函数的函数。
  • 使用包含特定操作码的内联程序集。

如果发生错误,纯函数可以使用revert()require()函数来还原潜在的状态更改。

声明纯函数,可以在函数声明里,添加pure关键字。

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

contract Test {
function getResult() public pure returns(uint product, uint sum){
uint a = 1;
uint b = 2;
product = a * b;
sum = a + b;
}
}

fallback(回退)函数:

fallback(回退) 函数是合约中的特殊函数。它有以下特点

  • 当合约中不存在的函数被调用时,将调用fallback函数。
  • 被标记为外部函数。
  • 它没有名字。
  • 它没有参数。
  • 它不能返回任何东西。
  • 每个合约定义一个fallback函数。
  • 如果没有被标记为payable,则当合约收到无数据的以太币转账时,将抛出异常。

语法

1
2
3
4
// 没有名字,没有参数,不返回,标记为external,可以标记为payable
function() external {
// statements
}

下面的示例展示了合约中的回退函数概念。

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
pragma solidity ^0.5.0;

contract Test {
uint public x ;
function() external { x = 1; }
}
contract Sink {
function() external payable { }
}
contract Caller {

function callTest(Test test) public returns (bool) {
(bool success,) = address(test).call(abi.encodeWithSignature("nonExistingFunction()"));
require(success);
// test.x 是 1

address payable testPayable = address(uint160(address(test)));

// 发送以太测试合同,
// 转账将失败,也就是说,这里返回false。
return (testPayable.send(2 ether));
}

function callSink(Sink sink) public returns (bool) {
address payable sinkPayable = address(sink);
return (sinkPayable.send(2 ether));
}
}

函数重载

同一个作用域内,相同函数名可以定义多个函数。这些函数的参数(参数类型或参数数量)必须不一样。仅仅是返回值不一样不被允许

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
pragma solidity ^0.5.0;

contract Test {
function getSum(uint a, uint b) public pure returns(uint){
return a + b;
}
function getSum(uint a, uint b, uint c) public pure returns(uint){
return a + b + c;
}
function callSumWithTwoArguments() public pure returns(uint){
return getSum(1,2);
}
function callSumWithThreeArguments() public pure returns(uint){
return getSum(1,2,3);
}
}

//0: uint256: 3
//0: uint256: 6

数学函数

Solidity提供内置的数学函数,如:

  • addmod(uint x, uint y, uint k) returns (uint) 计算(x + y) % k,计算中,以任意精度执行加法,且不限于2^256大小。
  • mulmod(uint x, uint y, uint k) returns (uint) 计算(x * y) % k,计算中,以任意精度执行乘法,且不限于2^256大小。

加密函数

Solidity 提供了常用的加密函数。以下是一些重要函数:

  • keccak256(bytes memory) returns (bytes32) 计算输入的Keccak-256散列。
  • sha256(bytes memory) returns (bytes32) 计算输入的SHA-256散列。
  • ripemd160(bytes memory) returns (bytes20) 计算输入的RIPEMD-160散列。
  • ecrecover(bytes32 hash, uint8 v, bytes32 r, bytes32 s) returns (address) 从椭圆曲线签名中恢复与公钥相关的地址,或在出错时返回零。函数参数对应于签名的ECDSA值: r – 签名的前32字节; s: 签名的第二个32字节; v: 签名的最后一个字节。这个方法返回一个地址。

合约继承

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

contract A {
...
}

contract B is A {
...
}

构造函数

构造函数是使用construct关键字声明的特殊函数,用于初始化合约的状态变量。合约中构造函数是可选的,可以省略。

构造函数有以下重要特性:

  • 一个合约只能有一个构造函数。
  • 构造函数在创建合约时执行一次,用于初始化合约状态。
  • 在执行构造函数之后,合约最终代码被部署到区块链。合约最终代码包括公共函数和可通过公共函数访问的代码。构造函数代码或仅由构造函数使用的任何内部方法不包括在最终代码中。
  • 构造函数可以是公共的,也可以是内部的。
  • 内部构造函数将合约标记为抽象合约。
  • 如果没有定义构造函数,则使用默认构造函数。
1
2
3
4
5
pragma solidity ^0.5.0;

contract Test {
constructor() public {}
}
  • 如果基合约具有带参数的构造函数,则每个派生/继承的合约也都必须包含参数。
  • 可以使用下面的方法直接初始化基构造函数
1
2
3
4
5
6
7
8
9
10
11
pragma solidity ^0.5.0;

contract Base {
uint data;
constructor(uint _data) public {
data = _data;
}
}
contract Derived is Base (5) {
constructor() public {}
}
  • 可以使用以下方法间接初始化基构造函数
1
2
3
4
5
6
7
8
9
10
11
pragma solidity ^0.5.0;

contract Base {
uint data;
constructor(uint _data) public {
data = _data;
}
}
contract Derived is Base {
constructor(uint _info) Base(_info * _info) public {}
}
  • 不允许直接或间接地初始化基合约构造函数。
  • 如果派生合约没有将参数传递给基合约构造函数,则派生合约将成为抽象合约。

抽象合约

1
2
3
4
5
6
7
8
9
10
11
12
13
14
pragma solidity ^0.5.0;

contract Calculator {
function getResult() public view returns(uint);
}

contract Test is Calculator {
function getResult() public view returns(uint) {
uint a = 1;
uint b = 2;
uint result = a + b;
return result;
}
}

接口

接口类似于抽象合约,使用interface关键字创建,接口只能包含抽象函数,不能包含函数实现。以下是接口的关键特性:

  • 接口的函数只能是外部类型。
  • 接口不能有构造函数。
  • 接口不能有状态变量。
  • 接口可以包含enum、struct定义,可以使用interface_name.访问它们。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
pragma solidity ^0.5.0;

interface Calculator {
function getResult() external view returns(uint);
}

contract Test is Calculator {
constructor() public {}
function getResult() external view returns(uint){
uint a = 1;
uint b = 2;
uint result = a + b;
return result;
}
}

Solidity中,对库的使用有一定的限制。以下是库的主要特征。

  • 如果库函数不修改状态,则可以直接调用它们。这意味着纯函数视图函数只能从库外部调用。
  • 库不能被销毁,因为它被认为是无状态的。
  • 库不能有状态变量。
  • 库不能继承任何其他元素。
  • 库不能被继承。

使用library声明,使用<library_name>.<func_name>

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
pragma solidity ^0.5.0;

library Search {
function indexOf(uint[] storage self, uint value) public view returns (uint) {
for (uint i = 0; i < self.length; i++) if (self[i] == value) return i;
return uint(-1);
}
}
contract Test {
uint[] data;
constructor() public {
data.push(1);
data.push(2);
data.push(3);
data.push(4);
data.push(5);
}
function isValuePresent() external view returns(uint){
uint value = 4;

// 使用库函数搜索数组中是否存在值
uint index = Search.indexOf(data, value);
return index;
}
}

****使用汇编(Assembly)代码****

使用内联汇编,可以在solidity中嵌入汇编代码,使用

1
2
3
assembly{
...
}

事件

事件是智能合约发出的信号。智能合约的前端UI,例如,DApps、web.js,或者任何与Ethereum JSON-RPC API连接的东西,都可以侦听这些事件。事件可以被索引,以便以后可以搜索事件记录。

事件在区块链中的存储

区块链是一个区块链表,这些块的内容基本上是交易记录。每个交易都有一个附加的交易日志,事件结果存放在交易日志里。合约发出的事件,可以使用合约地址访问。

Solidity中可以使用event关键字(在用法上类似于function关键字)。然后可以在函数中使用emit关键字触发事件

创建合约并发出一个事件

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

contract Counter {
uint256 public count = 0;

event Increment(address who); // 声明事件

function increment() public {
emit Increment(msg.sender); // 触发事件
count += 1;
}
}