Solidity 基础(二)

11/24/2023 Blockchain

摘要

心不死则道不生,倘若穷途末路,那便势如破竹,愿我们全力以赴,不留遗憾,身不苦,则福禄不厚,心不苦,则智慧不开。

# 一:全局变量和函数

# 1.1 var 类型变量

var 是一种只能在函数中声明的特殊类型。在合约中,状态变量不能是 var 类型。使用 var 类型声明的变量称为 隐式类型变量,因为 var 不显式表示任何类型。它通知编译器它的类型是由第一次赋给它的值决定的。一旦确定类型后,则无法更改。

pragma solidity 0.4.19;

contract VarExamples {

    function VarType() {
        var uintVar8 = 10; //uint8
        uintVar8 = 255; //256 is error

        var uintVar16 = 256; //uint16
        uintVar16 = 65535; //aaa = 65536; is error

        var intVar8 = -1; //int8 values -128 to 127

        var intVar16 = -129; //int16 values -32768 to 32767

        var boolVar = true;
        boolVar = false; // 10 is error, 0 is error, 1 is error, -1 is error

        var stringVar = "0x10"; // this is string memory
        stringVar = "10"; // cc =1231231231231231231212222222 is error

        var bytesVar = 0x100; // this is byte memory

        var Var = hex"001122FF";

        var arrayInteger = [uint8(1), 2];
        arrayInteger[1] = 255; 

        var arrayByte = bytes10(0x2222);
        arrayByte = 0x11111111111111111111; //0x111111111111111111111 is error
    }
}
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

# 1.2 变量声明提前

在使用变量之前不需要声明和初始化变量。变量声明可以在函数内的任何地方发生,即使在使用它之后也可以。这就是所谓的变量声明提前。Solidity 编译器提取函数内任意位置声明的所有变量,并将它们放在函数的顶部或开头。在 Solidity 中声明变量会使用各自的默认值进行初始化。这确保了在整个函数中变量都可用。

pragma solidity ^0.4.19;

contract variableHoisting {

    function hoistingDemo() returns (uint){
		// firstVar、secondVar、result 在函数的末尾声明,在函数的开头使用。
		// 它将所有的变量声明作为函数中的第一组指令
        firstVar = 10;
        secondVar = 20;

        result = firstVar + secondVar;

        uint firstVar;
        uint secondVar;
        uint result;
        return result;
    } 
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

# 1.3 变量作用域

作用域是指在 Solidity 中合约和函数中的变量可用性。Solidity 提供以下两个可以声明变量的位置:

  • 合约级全局变量 —— 也称为状态变量
  • 函数级局部变量

合约级全局变量可以附加一个可见性修改器。无论可见性修改器如何,都可以在整个网络中查看状态数据。以下状态变量只能使用函数进行修改:

  • public:可以直接从外部调用访问这些状态变量。编译器隐式生成 getter 函数以读取公共状态变量的值。
  • internal:无法直接从外部调用访问这些状态变量。可以从当前合约中的函数和此合约的子合约访问它们。
  • private:无法直接从外部调用访问这些状态变量,子合约也无法访问它们,只能通过当前合约中的函数访问。

# 1.4 类型转换

一种类型的变量到另一种类型的变量需要做某种转换,这称为类型转换。Solidity 提供了类型转换的规则。

  • 隐式转换,不需要操作符,或不需要外部帮助进行转换。这些类型的转换是完全合法的,数据不会丢失或者值不匹配。它们完全是类型安全的。Solidity 允许从较小的整数类型到较大的整数类型的隐式转换。例如,将 uint8 转换为 uint16 会隐式发生。
  • 显式转换,当由于数据丢失或包含不在目标数据类型范围的数据的值时,编译器不执行隐式转换,此时需要显式转换。Solidity 为每种值类型提供了显式转换的函数。如 uint16 转换为 uint8。在这种情况下可能会丢失数据。
pragma solidity ^0.4.19;

contract ConversionDemo {

    function ConvertionExplicitUINT8toUINT256() returns (uint) {
		uint8 myVariable = 10;
        uint256 someVariable = myVariable;
        return someVariable;
    }
    
    // 从 uint256 到 uint8 的显式转换。如果转换时隐式发生的,则此转换到引发编译时错误
    function ConvertionExplicitUINT256toUINT8() returns (uint8) {
        uint256 myVariable = 10;
        uint8 someVariable = uint8(myVariable);
        return someVariable;
    }
    
    // 编译器不会出错,但是会尝试将值拟合为较小的值并循环查找有效值
    function ConvertionExplicitUINT256toUINT81() returns (uint8) {
        uint256 myVariable = 10000134;
        uint8 someVariable = uint8(myVariable);
        return someVariable; //returns 6 as return value
    }
    
    function Convertions() {
        uint256 myVariable = 10000134;
        uint8 someVariable  = 100;
        bytes4 byte4 = 0x65666768;
       
        // bytes1 byte1 = 0x656667668; //error
        bytes1 byte1 = 0x65;
        
        //  byte1 = byte4; //error, explicit conversion needed here
        byte1 = bytes1(byte4) ; //explicit conversion
    
        byte4 = byte1;  //Implicit conversion 
        
        // uint8 someVariable = myVariable; // error, explicit conversion needed here
        
        myVariable = someVariable; //Implicit conversion 
        
        string memory name = "Ritesh";
        bytes memory nameInBytes = bytes(name); //explicit string to bytes conversion
        
        name = string(nameInBytes); //explicit bytes to string conversion
    }
}
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
40
41
42
43
44
45
46
47

# 1.5 区块和交易全局变量

Solidity 提供了几个全局变量的访问权限,这些变量未在合约中声明,但可以通过合约代码访问。合约无法直接访问账本。账本仅由矿工维护。但是 Solidity 提供了一些当前交易和区块的相关信息,以便可以使用它们。Solidity 提供了区块以及与交易相关的变量。

变 量 名 称 描 述
block.coinbase(address) 与 etherbase 相同,指矿工的地址
block.difficulty(uint) 当前区块难度等级
block.gaslimit(uint) 当前区块 gas 限制
block.number(uint) 顺序表示的区块编号
block.timestamp(uint) 区块创建时间
msg.data(bytes) 与创建交易相关的函数及其参数信息
msg.gas(uint) 执行交易后未花费 gas
msg.sender(address) 调用函数的地址
msg.sig(bytes4) 函数标识符使用散列函数答名后的前四个字节
msg.value(uint) 随交易发送的以太币数量,以 wei 作为单位
now(uint) 当前时间
tx.gasprice(uint) 调用者准备支付的每一种 gas 单位下的 gas 价格
tx.orgin(address) 交易的发起者
block.blockhash(uint blockNumber)
returns(bytes32)
包含交易的区块散列值
pragma solidity ^0.4.19;

contract TransactionAndMessageVariables {

    event logstring(string);
    event loguint(uint);
    event logbytes(bytes);
    event logaddress(address);
    event logbyte4(bytes4);
    event logblock(bytes32);
    
    function globalVariable() payable {
    
       logaddress(block.coinbase); // 0x94d76e24f818426ae84aa404140e8d5f60e10e7e
       
       loguint(block.difficulty); // 71762765929000
       
       loguint(block.gaslimit); // 6000000
       
       loguint(msg.gas); // 2975428
       
       loguint(tx.gasprice); // 1
       
       loguint(block.number); // 123
       
       loguint(block.timestamp); // 1513061946 
       
       loguint(now); // 1513061946
       
       logbytes(msg.data); // 0x4048d797
       
       logbyte4(msg.sig); // 0x4048d797
        
       loguint(msg.value); // 0 or in Wei if ether are send
        
       logaddress(msg.sender); // 0xca35b7d915458ef540ade6068dfe2f44e8fa733c
       
       logaddress(tx.origin); // 0xca35b7d915458ef540ade6068dfe2f44e8fa733c

       logblock(block.blockhash(block.number)); // 0x00000000000000000000000
    }
}
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
40
41
42

tx.origin 和 msg.sender 的区别

tx.origin 全局变量是指发起交易的原始外部账户,而 msg.sender 是指调用该函数的直接账户(可以是外部账户或其他合约账户)

tx.origin 变量将始终引用外部账户,而 msg.sender 可以是合约内账户或外部账户。

如果多个合约上有多个函数调用,则 tx.origin 将始终引用发起交易的账户,而不管调用的合约堆栈如何。但是,msg.sender 将引用调用下一个合约的先前一个账户(合约内 / 外部)。

建议优先使用 msg.sender

# 1.6 加密全局变量

Solidity 为合约提供了散列值的加密函数。有两个散列函数 —— SHA2 和 SHA3

  • sha3 函数基于 sha3 算法
  • sha2 函数基于 sha256 算法
  • keccak256 函数是 sha3 算法别名

建议当有需要使用散列的时候调用 keccak256 或 sha3 函数

pragma solidity ^0.4.19;

contract CryptoFunctions {
    function cryptoDemo() returns (bytes32, bytes32, bytes32){
		return (sha256("r"), keccak256("r"), sha3("r"));
    }
}
1
2
3
4
5
6
7

# 1.7 地址全局变量

外部的或基于合约的每个地址,都具有五个全局函数和一个全局变量。与地址相关的全局变量称为余额,它提供地址中以 wei 为单位的以太币余额。

函数如下:

  • <address>.transfer(uint256 amount):该函数向 address 发送给定的以 wei 为单位的以太币,如果执行失败则返回 false;
  • <address>.send(uint256 amount):该函数向 address 发送给定的以 wei 为单位的以太币,并在失败时返回 false;
  • <address>.call(...) returns (bool):此函数调用低级别的 call 函数,并在失败时返回 false;
  • <address>.ca11code(...) returns (bool):此函数调用低级别的 callcode 函数,失败时返回 false;
  • <address>.delegatecall(...) returns (bool):此函数调用低级别的 delegatecall 函数,失败时返回 false。

# 1.8 合约全局变量

每份合约都有以下三个全局函数:

  • this:当前合约的类型,可显式转换为地址;
  • selfdestruct:接收一个地址,销毁当前合约,将其资金发送到给定地址
  • suicide:接收一个地址,也是 selfdestruct 的别名。

# 二:表达式和控制结构

# 2.1 Solidity 表达式

比较运算符

操作符 含义
== 相等
!= 不相等
> 大于
< 小于
>= 大于或者等于
<= 小于或者等于

逻辑运算符

操作符 含义
&&
||
!

优先级

优先级 描述 运算符
1 后缀自增和自减 ++、--
新建表达式 new<typename> NA
数组下标 <array>[<index>] NA
成员访问 <object>.<member> NA
函数调用 <func>(<args...>) NA
圆括号 (<statement>) NA
2 前缀自增或自减 ++、--
一元正负号 +、- NA
一元运算 delete NA
逻辑非 ! NA
按位取反 ~ NA
3 求幂 **
4 乘法、除法和模运算 *、/、%
5 加法、减法 +、-
6 按位移位 <<、>>
7 按位与 &
8 按位异或 ^
9 按位或 |
10 不等式操作符 <、>、<=、>=
11 等式操作符 ==、!=
12 逻辑或 &&
13 逻辑与 ||
14 三元算子 <conditional>?<if-true>:<if-false>
15 赋值操作符 =、|=、^=、&=、<<=、>>=、+=、-=、*=、/=、%=
16 逗号操作符 ,

# 2.2 if 决策控制

pragma solidity ^0.4.19;

contract IfElseExample {

	enum requestState {created, approved, provisioned, rejected, deleted, none}

    function StateManagement(uint8 _state) returns (int result) {
        requestState currentState = requestState(_state);

        if (currentState == requestState(1)) {
        	result = 1;
        }else if ((currentState == requestState.approved) || (currentState == requestState.provisioned)) {
        	result = 2;
        } else {
        	currentState == requestState.none;
        	result = 3;
        }
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

# 2.3 while 循环

pragma solidity ^0.4.19;

contract whileLoop {
    
	mapping (uint => uint) blockNumber;
	uint counter;

	event uintNumber(uint);
	bytes aa;

	function SetNumber()  {
		blockNumber[counter++] = block.number;
	}

	function getNumbers() {
		uint i = 0;
		while (i < counter) {
			uintNumber(blockNumber[i]);
			i = i + 1;
		}
	}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

# 2.4 for 循环

pragma solidity ^0.4.19;

contract ForLoopExample {

    mapping (uint => uint) blockNumber;
    uint counter;

    event uintNumber(uint);

    function SetNumber() {
    	blockNumber[counter++] = block.number;
    }

    function getNumbers() {
        for (uint i=0; i < counter; i++){
        	uintNumber(blockNumber[i]);
        }
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

# 2.5 do...while 循环

while 和 do...while 循环之间存在细微差别。do...while 循环中的至少执行一次指令。

pragma solidity ^0.4.19;

contract DowhileLoop {

    mapping (uint => uint) blockNumber;
    uint counter;

    event uintNumber(uint);
    bytes aa;

    function SetNumber()  {
    	blockNumber[counter++] = block.number; 
    }

    function getNumbers() {
        uint i = 0;
        do {
            uintNumber(blockNumber[i]);
            i = i + 1;
        } while (i < counter);
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

# 2.6 break 语句

终止循环

pragma solidity ^0.4.19;

contract ForLoopExampleBreak {
    
    mapping (uint => uint) blockNumber;
    uint counter;

    event uintNumber(uint);

    function SetNumber()  {
    	blockNumber[counter++] = block.number;
    }

    function getNumbers() {
        for (uint i=0; i < counter; i++){
            if (i == 1)
                break;
            uintNumber(blockNumber[i]);
        }
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

# 2.7 continue 语句

跳过本次循环

pragma solidity ^0.4.19;

contract ForLoopExampleContinue {
    
	mapping (uint => uint) blockNumber;
	uint counter;

	event uintNumber(uint);

	function SetNumber()  {
		blockNumber[counter++] = block.number;
	}

	function getNumbers() {
		for (uint i=0; i < counter; i++){
			if ((i > 5) ) { 
				continue;
			}
			uintNumber(blockNumber[i]);
		}
	}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

# 2.8 return 语句

Solidity 为函数返回数据提供了两种不同的语法。

pragma solidity ^0.4.19;

contract ReturnValues {
	uint counter;

	function SetNumber()  {
		counter = block.number;
	}

	// 返回 uint 而不命名 return 变量
	// 这种情况下,可以显式使用 return 关键字从函数返回
	function getBlockNumber() returns (uint) {
		return counter;
	}

	// 返回 uint,并提供变量的名称
	// 这种情况下,可以直接使用并从函数返回此变量
	function getBlockNumber1() returns (uint result) {
		result =  counter;
	}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

# 三:编写智能合约

# 3.1 智能合约

智能合约本质上是在 EVM 中部署和执行的代码段或程序。智能合约是区块链术语;它是一句行话,用于指代在 EVM 中执行的编程逻辑和代码。

# 3.2 编写一个简单的合约

pragma solidity 0.4.19;

// 合约定义
contract generalStructure {

	int public stateIntVariable;
	string stateStringVariable;
	address personIdentifier;
	myStruct human;
	bool constant hasIncome;
	
	struct myStruct {
		string name;
		uint myAge;
		bool isMarried;
		uint[] bankAccountsNumbers;
	}
	
	modifier onlyBy() {
		if (msg.sender == personIdentifier) {
			_;
		}
	}
	
	event ageRead(address, int);
	
	enum gender {male, female}
	
	function getAge (address _personIdentifier) onlyBy() payable external returns (uint) {
		human = myStruct("Ritesh", 10, true, new uint[](3));
		gender _gender = gender.male;
		ageRead(personIdentifier, stateIntVariable);
	}

}
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

# 3.3 创建合约

在 Solidity 中有两种方法创建并使用合约:

  1. 使用 new 关键字,通过部署合约来初始化合约实例,初始化状态变量,运行其构造函数,将 nonce 值设置为1,并最终将实例的地址返回给调用者。部署合约涉及检查请求者是否提供了足够的 gas 来完成部署,使用请求者的 address 和 nonce 值生成合约部署的 新账户/地址,并花费该地址上的以太币。
例子
pragma solidity 0.4.19;

contract HelloWorld {
    uint private simpleInt;

    function getValue() public view returns (uint) {
    	return simpleInt;
    }

    function setValue(uint _value) public {
    	simpleInt = _value; 
    }
}

contract client {

    function useNewKeyword() public returns (uint) {
        HelloWorld myObj = new HelloWorld();
        myObj.setValue(10);
        return myObj.getValue();
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
  1. 使用已经部署的合约地址,当已经部署和实例化合约时,使用此方法来创建合约实例。这种方法使用现有的已部署合约的地址。没有创建新实例,相反,现有实例被重用。使用其地址对现有合约进行引用。
例子
pragma solidity 0.4.19;

contract HelloWorld {
	uint private simpleInt;
	
	function GetValue() public view returns (uint) {
		return simpleInt;
	}
	
	function SetValue(uint _value) public {
		simpleInt = _value;
	}
}

contract client {
	address obj;
	
	function setObject(address _obj) external {
		obj = _obj;
	}
	
	function UseExistingAddress() public returns (uint) {
		// 使用另一个合约的已知地址来创建 HelloWorld 的引用
		// myObj 对象包含现有合约的地址
		HelloWorld myObj = HelloWorld(obj);
		myObj.SetValue(10);
		return myObj.GetValue();
	}
}
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

# 3.4 构造函数

构造函数是可选的,当没有显式定义构造函数时,编译器会合成默认构造函数。构造函数在部署合约时执行一次。构造函数代码是合约执行的第一组代码。与其他编程语言不同,合约中最多只能有一个构造函数。可以在部署合约时为构造函数提供参数。

构造函数与合约同名,且只能是 public 或 internal。构造数不会显式返回任何数据。

pragma solidity 0.4.19;

contract HelloWorld {

	uint private simpleInt;
	
	function HelloWorld() public {
		simpleInt = 5;
	}
	
	function GetValue() public view returns (uint) {
		return simpleInt;
	}
	
	function SetValue(uint _value) public {
		simpleInt = _value;
	}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

# 3.5 合约组合

Solidity 支持合约组合。组合是指将多个合约或数据类型组合在一起以创建复杂的数据结构和合约。

# 3.6 继承

被继承的合约称为父合约,继承的合约称为子合约。同样,有父合约的合约称为派生合约,父合约称为基合约。继承主要是关于代码的可重用性。基合约和派生合约之间存在 is-a 关系,所有公共和内部范围函数和状态变量都可用于派生合约。实际上,Solidity 编译器将基合约字节码复制到派生合约字节码中is 关键字用于在派生合约中继承基合约。

Solidity 支持多种类型的继承,包括多重继承。Solidity 将基合约复制到派生合约中,并使用继承创建单个合约。生成单个地址,该地址在父子关系的合约之间共享。

单继承有助于将基合约的变量、函数、修改器和事件继承到派生合约中。

pragma solidity 0.4.19;

contract ParentContract {
	uint internal simpleInteger;
	
	function SetInteger(uint _value) external {
		simpleInteger = _value;
	}
}

contract ChildContract is ParentContract {
	bool private simpleBool;
	
	function GetInteger() public view returns (uint) {
		return simpleInteger;
	}
}

contract Client {
	ChildContract pc = new ChildContract();
	
	function workWithInheritance() public returns (uint) {
		pc.SetInteger(100);
		return pc.GetInteger();
	}
}
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

多级继承与单继承非常相似。但是,不仅仅是一个父子关系,还有多个级别的父子关系。

分层继承也类似于单继承。但是,在这里,单个合约充当多个派生合约的基合约。

Solidity 支持多重继承。单个继承可以有多个级别。但是,也可能存在多个相同基合约的派生合约。这些派生合约可以一起作为深一层子类的基合约。当从这些子合约一起继承时,发生多重继承。

Solidity 遵循 Python 的路径并使用 C3线性化,也称为 方法解析顺序(MRO),来强制基合约图中的特定顺序。合约应该在继承时遵循特定的顺序,从基合约到最远派生的合约。

pragma solidity 0.4.19;

conract SumContract {
	function Sum(uint a, uint b) public returns (uint) {
		return a + b;
	}
}

contract MultiContract is SumContract {
	function Multiply(uint a, uint b) public returns (uint) {
		return a * b;
	}
}

contract DivideContract is SumContract {
	function Multiply(uint a, uint b) public returns (uint) {
		return a / b;
	}
}

contract SubContract is SumContract, MultiContract, DivideContract {
	function sub(uint a, uint b) public returns (uint) {
		return a - b;
	}
}

contratc client {
	function workWithInheritance() public returna (uint) {
		uint a = 20;
		uint b = 10;
		SubContract subt = new SubContract();
		return subt.Sum(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

# 3.7 继承

封装是指对改变其状态的处理,允许隐藏或直接访问状态变量。它指的是声明变量的模式,客户端无法直接访问这些变量,只能使用函数进行修改。这有助于约束对变量的访问,但同时允许对类进行足够的访问以对其进行操作。

# 3.8 多态性

多态性意味着具有多种形式。有两种类型的多态性:

  • 函数多态性,同一个合约中声明多个同名函数或继承具有同名函数的合约。同名函数在参数数据类型或参数个数上有所不同。
    • 为了确定多态性的有效函数签名,不考虑返回类型。这也被称为方法重载
    例子
pragma solidity ^0.4.19;

contract HelloFunctionPloymorphism {
	function getVariableData(int8 data) public constant returns(int8 output) {
		return data;
	}

	function overloadedFunction(int16 data) public constant retruns(int16 output) {
		return data;
	}
}
1
2
3
4
5
6
7
8
9
10
11

:::

  • 合约多态性,指当合约通过继承相互关联时,可以使用多个合约实例进行互换。合约多态有助于使用基类合约实例调用派生合约函数。
例子






















 







pragma solidity ^0.4.19;

contract ParentContract {
	uint internal simpleInteger;

	function SetInteger(uint _value) public {
		simpleInteger = _value;
	}

	function GetInteger() public view returns (uint) {
		return 10;
	}
}

contract ChildContract is ParentContract {
  
	function GetInteger() public view returns (uint) {
		return simpleInteger;
	}
}

contract Client {
	ParentContract pc = new ChildContract();
  
	function workWithInheritance() public returns (uint) {
		pc.SetInteger(100);
		return pc.GetInteger();
	}
}
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

# 3.9 方法覆盖

方法覆盖是指在派生合约中重新定义父合约中具有相同名称和签名的数。

# 3.10 抽象合约

抽象合约是具有部分函数定义的合约。无法创建抽象合约的实例。为了使用其函数,抽象合约必须由子合约继承。抽象合约有助于定义合约的结构,任何继承类都必须确保提供它们自己的实现。如果子合约没有提供待完善函数的实现,则无法创建它的实例。函数签名使用分号字符结束。如果合约具有未实现的函数,则它将成为抽象类。

pragma solidity 0.4.19;

// 抽象合约
contract abstractHelloWorld {
	// 没有实现的函数签名
	function GetValue() public view returns (uint);
	function SetValue(uint _value) public;

	function AddNumber(uint _value) public returna (uint) {
		return 10;
	}
}

// 合约继承,且提供所有方法的实现
contract HelloWorld is abstractHelloWorld {
	uint private simpleInteger;

	function GetValue() public view returns (uint) {
		return simpleInteger;
	}

	function SetValue(uint _value) public {
		simpleInteger = _value;
	}

	function AddNumber(uint _value) public returns (uint){
		returns simpleInteger = _value;
	}
}

contract client {
	abstractHelloWorld myObj;

	function client() {
		myObj = new HelloWorld();
	}

	function GetIntegerValue() public returns (uint) {
		myObj.SetValue(100);
		return myObj.AddNumber(200);
	}
}
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
40
41
42

# 3.11 接口

接口就像抽象合约,但有些差异。接口不能包含任何定义。它们只能包含函数声明。这意味着接口中的函数不能包含任何代码。它们也被称为纯抽象合约。接口只能包含函数的签名。它也不能包含任何状态变量。它们不能继承其他合约或包含枚举或结构。但是,接口可以继承其他接口。函数签名使用分号字符结束。接口使用 interface 关键字声明,后跟标识符。

pragma solidity 0.4.19;

// 接口
interface IHelloWorld {
	function GetValue() public view returns (uint);
	function SetValue(uint _value) public;
}

contract HelloWorld is IHelloWorld{
	uint private simpleInteger;

	function GetValue() public view returns (uint) {
		return simpleInteger;
	}

	function SetValue(uint _value) public {
		simpleInteger = _value;
	}
}

contract client {
	IHelloWorld myObj;

	function client(){
		myObj = new HelloWorld();
	}

	function GetSetIntegerValue() public returns (uint) {
		myObj.SetValue(100);
		return myObj.GetValue();
	}
}
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

# 四:函数、修改器和fallback函数

# 4.1 函数输入和输出
















 






 



pragma solidity ^0.4.19;

contract Parameters {

	// 单输入,单返回
    function singleIncomingParameter(int _data) returns (int _output) {
        return _data * 2;
    }

	// 多输入,单返回
    function multipleIncomingParameter(int _data, int _data2) returns (int _output) {
    	return _data * _data2;
    }

	// 单输入,多返回
    function multipleOutgoingParameter(int _data) returns (int square, int half) {
        square = _data * _data;
        half = _data /2 ;
    }

	// 返回元组。元组是由多个变量组成的自定义数据结构
    function multipleOutgoingTuple(int _data) returns (int square, int half) {
    	(square, half) = (_data * _data,_data /2 );
    }
}
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

# 4.2 修改器

pragma solidity ^0.4.17;

contract ContractWithoutModifier {

	address owner;
	int public mydata;

	// 全局变量 msg.sender 用于在所有者状态变量中输入账户值
	function ContractWithoutModifier(){
		owner = msg.sender;
	}

	// 检查调用者是否与部署合约的账户相同,如果相同,则执行函数代码
	function AssignDoubleValue(int _data) public  {
		if(msg.sender == owner) {
			mydata = _data * 2;
		}
	}

	// 检查调用者是否与部署合约的账户相同,如果相同,则执行函数代码
	function AssignTenerValue(int _data) public  {
		if(msg.sender == owner) {
			mydata = _data * 10;
		}
	}
}
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

修改器是改变函数行为的特殊函数。修改器只能应用于函数。修改上面代码:

pragma solidity ^0.4.17;

contract ContractWithModifier {
    
	address owner;
	int public mydata;

	function ContractWithoutModifier(){
		owner = msg.sender;
	}

	modifier isOwner {
		// require(msg.sender == owner);
		if(msg.sender == owner) {
			_;
		}
	}

	function AssignDoubleValue(int _data) public isOwner {
		mydata = _data * 2;
	}

	function AssignTenerValue(int _data) public  {
		mydata = _data * 10;
	}
}
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

# 4.3 view 函数、constant 函数和 pure 函数

Solidity 为函数提供了特殊修改器,例如 view、pure 和 constant。这些也称为 状态可变 属性,因为它们定义了以太坊全局状态中允许修改的范围。

编写智能合约函数主要有以下三项活动:

  • 更新状态变量
  • 读取状态变量
  • 逻辑执行

函数的执行和交易并不是免费的,需要花费gas。每笔交易都需要花费与其 执行相对应的一定量的gas,调用者需要负责提供足量的gas以便成功执行。交 易或修改以太坊全局状态的任何活动都如此

以太坊的文档(https://docs.soliditylang.org/en/v0.4.21/contracts.html (opens new window))提到以下与修改状态相关的语句:

  • 写状态变量
  • 发出事件
  • 创建其他合约
  • 使用 selfdestruct
  • 通过调用发送以太币
  • 调用任何未标记为 view 或 pure 的函数
  • 使用低级调用
  • 使用包含某些操作码的内联汇编

Solidity 开发人员可以使用 view 修改器标记其函数,以建议 EVM 此函数不会更改以太坊状态或之前提及的任何活动。目前,非强制要求。

pragma solidity ^0.4.17;

contract ViewFunction {
    function GetTenerValue(int _data) public view returns (int)  {
    	return _data * 10;
    }    
}
1
2
3
4
5
6
7

如果函数没有修改任何状态只返回值,则可以使用 view 标记它。view 函数也称为常量函数。

pure 函数同 view 函数目的相同,即限制状态的可变性。同样编写代码时非强制要求。pure 函数在 view 函数之上添加了进一步的限制,例如,不允许 pure 函数读写以太坊的全局状态。其他不允许的活动包括:

  • 读取状态变量
  • 访问 this.balance 或 <address>.balance
  • 访问 block、tx 和 msg 的任何成员(msg.sig 和 msg.data 除外)
  • 调用任何未标记为 pure 的函数
  • 使用包含某些操作码的内联汇编
pragma solidity ^0.4.17;

contract PureFunction {
    function GetTenerValue(int _data) public pure returns (int)  {
    	return _data * 10;
    }
}
1
2
3
4
5
6
7

# 4.4 地址相关函数

地址提供了五个函数和一个属性。地址提供的唯一属性是 balance 属性,它提供以 wei 为单位计算的账户(合约或个人)中可用的余额,如 <<account>>.balance,其中 account 是一个有效的以太坊地址。它将返回以 wei 为单位计算的余额。

  1. send 函数,将以太币发送到合约或个人拥有的账户;
    • <<account>>.send(amount)
    • 提供了不能被取代的 2300 个 gas 的固定费用;
    • 返回布尔值 true/false,不返回异常;
    例子
mapping (address => uint) balance;

function SimpleSendToAccount(uint amount) public returns (bool) {
	// 检查调用者是否具有足够的余额来提取资金
    if(balance[msg.sender] >= amount) {
    	// 如果有足够资金,将现有余额减去指定数量的金额
        balance[msg.sender] -= amount;
        // 调用 send 方法,检查是否成功
        if (msg.sender.send(amount) == true) {
        	return true;
    	} else {
    		// 调用 send 方法失败,退回余额
	    	balance[msg.sender] += amount;
    		return false;
    	}
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

:::

  1. transfer 函数,类似于send方法,它负责将以太币或 wei 发送到一个地址。但是,区别在于,在执行失败的情况下,transfer会引发异常,而不是返回false,并且所有更改都将被还原。
例子
function SimpleTransferToAccount() public {
	msg.sender.transfer(1);
}
1
2
3
  1. call 函数
    • web3.eth 对象的 call 方法,只能调用它所连接的节点,并且是只读操作。不允许改变以太坊的状态。不会产生交易,不消耗任何 gas。用于调用 pure 函数、constant 函数和 view 函数;
    • <<address>>.call 可以调用合约中任何可用的函数。有时候合约接口(通常称为ABI)不可用,因此调用函数的唯一方法是使用 call 方法。此方法不依附于 ABI,可以根据需要调用任何函数。这些调用没有编译时检查,返回布尔值;
    拓展

    合约中的每个函数都在运行时使用4字节标识符进行标识。这个4字节的标识符是函数名称及其参数类型的精简散列。在对函数名称和参数类型进行散列后,前四个字节被视为函数标识符。call 函数接受这些字节以将函数作为第一个参数调用,将实际参数值作为后续参数调用

// 无参调用函数
myaddr.call(bytes4(sha3("SetBalance()")));
// 带参调用函数
myaddr.call(bytes4(sha3("SetBalance(uint256)")), 10);
1
2
3
4

send 函数实际上内部调用 call 函数,入参并不需要提供 gas

pragma solidity ^0.4.17;

contract EtherBox {
    uint balance;

    function SetBalance() public {
    	balance = balance + 10;
    }

    function GetBalance() public payable returns(uint) {
    return balance;
    }
}

contract UsingCall {
    function UsingCall() public payable  {
    }

	// 创建 EtherBox 合约的实例并将其转换为地址。
	// 使用这个地址,call 函数调用 EtherBox 合约中的 SetBalance 函数
    function SimpleCall() public returns (uint) {
        bool status = true;
        EtherBox eb = new EtherBox();
        address myaddr = address(eb);
        status = myaddr.call(bytes4(sha3("SetBalance()")));
        return eb.GetBalance();
    }

	// 创建 EtherBox 合约的实例并将其转换为地址。
	// 使用这个地址,call 函数调用 EtherBox 合约中的 SetBalance 函数
	// 如果函数的执行需要更多 gas,跟随 call 函数一起发送 gas
    function SimpleCallwithGas() public returns (bool) {
        bool status = true;
        EtherBox eb = new EtherBox();
        address myaddr = address(eb);
        status = myaddr.call.gas(200000)(bytes4(sha3("GetBalance()")));
        return status;
    }

	// 此函数创建 EtherBox 合约的实例并将其转换为地址
	// 使用这个地址,call 函数调用 EtherBox 合约中的 SetBalance 函数
	// 如果函数的执行需要更多gas,跟随call函数一起发送 gas
	// 除了gas之外,还可以将向支付函数发送以太币或wei(以太币最小货币单位)
    function SimpleCallwithGasAndValue() public returns (bool) {
        bool status = true;
        EtherBox eb = new EtherBox();
        address myaddr = address(eb);
        status = myaddr.call.gas(200000).value(1)(bytes4(sha3("GetBalance()")));
        return status;
    }
}
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
40
41
42
43
44
45
46
47
48
49
50
51

:::

  1. callcode 函数,此函数已弃用。
  2. delegatecall 函数,此函数是一个低级函数,负责使用调用者的状态变量调用另一个合约中的函数。通常,它与 Solidity 中的库一起使用。详情可查看 https://docs.soliditylang.org/en/develop/introduction-to-smart-contracts.html (opens new window)

# 4.5 fallback 函数

当没有函数名称与被调用函数匹配时,将调用 fallback 函数。fallback 函数没有标识符或函数名称。它是定义的无名函数。由于无法显式调用,因此无法接受任何参数或返回任何值。

pragma solidity ^0.4.17;

contract FallbackFunction {
    function() {
    	var a = 0;
    }
}
1
2
3
4
5
6
7

当合约收到任何以太币时,也可以调用 fallback 函数。这通常发生在使用 web3 中提供的 SendTransaction 函数将以太币从一个账户发送给合约的情况下。但是,在这种情况下,fallback 函数应该标记为 payable,否则它将无法接受以太币并且将引发错误。

执行 fallback 函数需要多少 gas。由于无法明确调用,因此无法将 gas 发送到此函数。相反,EVM 为此功能提供了 2300 个gas的固定费用。任何超过此限制的 gas 消耗都会引发异常,并且在消耗了与原始函数一起发送的所有 gas 后,状态将被回滚。因此,重要的是测试你的 fallback 函数,以确保它不会消耗超过 2300 个gas。

fallback 函数是智能合约中安全漏洞的主要原因之一。在发布生产合约之前,从安全角度测试此函数是非常重要的。

pragma solidity ^0.4.17;

contract EtherBox {
	uint balance;
	event logme(string);

	function SetBalance() public {
		balance = balance + 10;
	}

	function GetBalance() public payable returns(uint) {
		return balance;
	}
	
	function() payable {
		logme("fallback called");
	}
}

contract UsingCall {
	function UsingCall() public payable  {}

	function SimpleCall() public returns (uint) {
		bool status = true;
		EtherBox eb = new EtherBox();
		address myaddr = address(eb);
		status = myaddr.call(bytes4(sha3("SetBalance()")));
		return eb.GetBalance();
	}

	function SimpleCallwithGas() public returns (bool) {
		bool status = true;
		EtherBox eb = new EtherBox();
		address myaddr = address(eb);
		status = myaddr.call.gas(200000)(bytes4(sha3("GetBalance()")));
		return status;
	}

	function SimpleCallwithGasAndValue() public returns (bool) {
		bool status = true;
		EtherBox eb = new EtherBox();
		address myaddr = address(eb);
		return status = myaddr.call.gas(200000).value(1)(bytes4(sha3("GetBalance()")));
	}
	
	function SimpleCallwithGasAndValueWithWrongName() public returns (bool) {
		bool status = true;
		EtherBox eb = new EtherBox();
		address myaddr = address(eb);
		// 调用不存在的函数名
		return myaddr.call.gas(200000).value(1)(bytes4(sha("GetBalance1()")));
	}
}
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
40
41
42
43
44
45
46
47
48
49
50
51
52
53

使用 send 方法,使用 web3SendTransaction 函数或 transfer 方法时也会 fallback函数。

# 五:异常、事件与日志

# 5.1 错误处理

常见的运行时错误有 gas 耗尽、零除错误、数据类型溢出、数组越界等。

从 4.10 版本开始,一些新的错误处理结构比如 assert、require 和 revert 等被引用,throw 语句也被标记为过时。在 Solidity 中并没有 try...catch 这种语句。

  1. require,意味着在运行接下来的代码之前,所声明的约束必须首先被满足。接受一个参数:执行结果为布尔值。如果该语句的执行结果为 false,异常会被抛出并且程序执行会被挂起,未消耗的 gas 会被退回给合约调用者并且合约状态会被回退到初始状态。require 语句导致 revert 指令的执行,用来回退合约状态并退回未消耗的 gas。
例子
pragma solidity ^0.4.19;

contract RequireContract {
	
	function ValidInt8(uint _data) public returns(uint8){
		require(_data >= 0);
		require(_data <= 255);
		return uint8(_data);
	}

	function ShouldbeEven(uint _data) public returns(bool){
		require(_data % 2 == 0);
		return true;
	}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
  1. assert,和 require 类似。未消耗的 gas 不会被退回给调用者而是被 assert 语句完全消费掉,合约会被回退到初始状态。assert 语句会产生一个 invalid 指令用来负责回退状态与耗尽 gas。
例子
pragma solidity ^0.4.19;

contract AssertContract {

	function ValidInt8(uint _data) public returns(uint8) {
		require(_data >= 0);
		require(_data <= 255);
		uint8 value = 20;

		//checking datatype overflow
		assert (value + _data <= 255);

		return uint8(_data);
	}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
  1. revert,和 require 类似。不同的是,它并不去检查条件也不依赖于任何状态或条件。碰到 revert 语句意味着异常会被抛出,未消耗的 gas 被退回,同时合约状态被恢复。
例子
pragma solidity ^0.4.19;

contract RevertContract {

	function ValidInt8(int _data) public returns(uint8){
		if(_data < 0 || _data > 255) {
			revert();
		}
		return uint8(_data);
	}
}
1
2
3
4
5
6
7
8
9
10
11

require 被用来校验外部传入的值,assert 应该被用来在合约和函数执行前验证其当前状态与条件。可以把 assert 当作是专门用来处理无法预测的运行时异常。在认为当前状态已经变得不一致而不适合继续运行时使用 assert。

# 5.2 事件与日志

事件是指合约中的某些更改,这些更改会引发事件发送并与事件监听者相互通知,以便它们可以行动起来并执行其他功能。

在 Solidity 中声明事件与执行函数非常相似。但是,事件没有任何代码体。一个简单的事件可以使用 event 关键字后跟一个标识符以及它想要随事件一起发送的任何参数来声明,如:event LogFunctionFlow(string),其中 event 是用于声明事件的关键字,后面跟着它的名称和一组将随该事件一起发送的参数。任何字符串文本都可以使用 LogFunctionFlow 事件发送。使用事件只需使用其名称调用事件并传递它所期望的参数即可。如:LogFunctionFlow("I am within function x");

pragma solidity ^0.4.19;

contract EventContract {
    
	event LogFunctionFlow(string);

	function ValidInt8(int _data) public returns(uint8){
		LogFunctionFlow("Within function ValidInt8");

		if(_data < 0 || _data > 255) {
			revert();
		}

		LogFunctionFlow("Value is within expected range");
		LogFunctionFlow("Returning value from function");

		return uint8(_data);
	}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
  1. 监听单个事件:在使用web3的方法中,可以监听和跟踪合约中的单个事件。当有确切事件是从合约中触发的时候,它有助于在web3客户端执行一个函数功能。
// ageRead 监听的事件名称。从编号 2500 读取,直到最新块
// 首先创建一个到 ageRead 事件的引用并将监听者添加到这个引用中
// 监听者需要一个在 ageRead 事件触发时执行的 promise 函数
var myEvent = instance.ageRead({fromBlock: 25000, toBlock: 'latest'});
myEvent.watch(function(error, result) {
	if(error) {
		console.log(error);
	} 
	console.log(result.args);
});
1
2
3
4
5
6
7
8
9
10
  1. 监听所有事件:在使用web3的方法中,可以监听和跟踪来自合约的所有事件。当合约触发任何事件时,它会通知并帮助在web3客户端执行一项功能作为回应。在这种情况下,可以使用事件名称过滤事件。
// 从编号 24000 读取,直到最新块
// 首先创建一个对 allEvents 的引用并将监听者添加到这个引用中
// 监听者随后接受一个 promise 函数
var myEvent = instance.allEvents({fromBlock: 24000, toBlock: 'latest'});
myEvent.watch(function(error, result){
	if(error) {
		console.log(error);
	} 
	console.log(result);
});
1
2
3
4
5
6
7
8
9
10

# 六:Truffle 基础与单元测试

# 6.1 应用程序开发生命周期管理

编程语言需要丰富的工具生态来简化开发。与任何应用程序一样,即使是基于区块链的去中心化应用也应该具有最简化的 应用程序生命周期管理(ALM) 过程。对于提高生产力,还可以引入用于智能合约的 DevOps 过程。Truffle 就是这样的一种工具,它可以使开发、测试和部署这些活动变得轻而易举。

# 6.2 Truffle

Truffle 是一种加速器,可帮助提高开发、部署和测试的速度,并提高开发人员的生产力。它专为基于以太坊的合约和应用程序开发而构建。它是一个基于 node 运行时的框架,可以帮助轻松实施 DevOps,持续集成、持续交付和持续部署。具体安装过程和使用过程可以查看 Truffle

# 七:合约调试

# 7.1 调试

调试合约有下面几种方式:

  • 使用 Remix 编辑器
  • 事件
  • BlockExplorer

# 7.2 使用 Block Explorer

Block Explorer 是一个以太坊浏览器。它提供有关其网络中当前区块和交易的报告和信息。这是了解有关现有和过去更多数据信息的好地方。https://etherscan.io/ (opens new window) 就是个Block Explorer,如下所示:

它显示涉及账户和合同的交易。单击某个交易会显示有关它的详细信息,如下所示.

  • Transaction Hash:此详细信息指的是交易散列;
  • Status:交易的状态,成功或失败;
  • Block:交易所存储区块的区块编号;
  • Timestamp:交易时间戳;
  • From:是谁发送了这笔交易;
  • To:交易的接收者;
  • Value:发送的以太币数量;
  • Gas Limit:用户指定的 gas 限定数量;
  • Gas Used By Txn:交易实际消耗的 gas 数量;
  • Gas Price:由交易发送者制定的 gas 价格;
  • nonce:是为了确定交易发送者所发送交易的数量;
  • Actual Tx Cost/Fees:显示了交易总成本,即 gasused*gasprice。

单击区块可显示有关区块的信息以及属于该区块的交易列表。它显示了区块头中的所有详细信息,例如区块散列,父区块散列,矿工账户,难度级别,随机数等,如下所示:

区块头里有一些有趣的属性,其中有一些会在这里提到。比如提供了在账本中区块编号的 Height、区块内的交易数,以及内部交易的数量(这些在合约之间被称为消息调用),当前区块头的散列值(Hash)、父块的散列值(Parent Hash)、叔块的根散列值、挖掘区块的 coinbase 或 etherbase 账户(Mined By)、当前区块的难度级别、到目前区块的累积难度、区块的大小、区块内所有交易使用的总 gas 量、区块的最大gas限量、用来证明工作证明的证据(Nonce)、以及挖矿奖励等。

# 八:参考文献

  • 《Solidity编程 构建以太坊和区块链智能合约的初学者指南 - 瑞提什·莫迪》
最后更新: 12/13/2023, 3:48:37 PM