認識 Solidity 基礎語法

前言

本文將認識 Solidity 的基礎語法,並使用 Remix IDE 線上編輯器進行撰寫。

建立專案

在編輯器上建立一個 MyContract.sol 檔。

許可標識

每份合約檔案都應添加其許可標識在開頭的註解中。

1
// SPDX-License-Identifier: MIT

編譯指示

關鍵字 pragma 是編譯指示,用來啟用編譯器檢查,編譯指示通常只對該檔案有效,所以需要把這個編譯指示添加到專案中所有的原始檔。

版本標識使用如下:

1
pragma solidity ^0.8.0;

如此標示,代表合約檔案將既不允許低於 0.8.0 版本的編譯器編譯,也不允許高於(包含)0.9.0 版本的編譯器編譯(因為使用了 ^ 符號)。這種做法的考慮是,編譯器在 0.9.0 版本之前不會有重大變更,所以可確保合約檔案始終按預期被編譯。

基礎語法

建立一個 MyContract 合約。

1
2
3
4
5
6
7
// SPDX-License-Identifier: MIT

pragma solidity ^0.8.0;

contract MyContract {
//
}

建立一個 myValue 變數。

1
2
3
contract MyContract {
string myValue;
}

建立一個建構子,並且設置 myValue 變數的預設值。

1
2
3
4
5
6
7
contract MyContract {
string myValue;

constructor() {
myValue = "My Value";
}
}

建立一個 Getter 和一個 Setter,以存取 myValue 變數。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
contract MyContract {
string myValue;

constructor() {
myValue = "My Value";
}

function get() view public returns(string memory) {
return myValue;
}

function set(string memory _myValue) public {
myValue = _myValue;
}
}

但是在智能合約中,呼叫函式是需要花費 Gas 費用的,因此可以簡化如下:

1
2
3
4
5
6
7
contract MyContract {
string public myValue = "My Value";

function set(string memory _myValue) public {
myValue = _myValue;
}
}

如果 myValue 是不可變的,可以將其定義為常數,如下:

1
2
3
contract MyContract {
string public constant myValue = "My Value";
}

簡單型別

int 整數

1
2
3
int8 amount = 100; // -127 ~ 127
int256 amount = 100;
int amount = 100; // 是 int256 的別名

uint 無符號整數

1
2
3
uint8 amount = 100; // 0 ~ 255
uint256 amount = 100;
uint amount = 100; // 是 uint256 的別名

address 位址

1
2
address addr;
address addr = 0xa77451687Ee77cB3DFf16A24446C54DB76C80222;

bytes 位元組

1
2
3
bytes public myBytes;
myBytes.length;
myBytes.push(hex"ff");

string 字串

1
2
string myName = "Memo Chou";
delete myName;

列舉

使用 enum 關鍵字建立一個列舉,值從 0 開始。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
contract MyContract {
enum State { Waiting, Ready, Active } // 0, 1, 2

State public state;

constructor() {
state = State.Waiting;
}

function activate() public {
state = State.Active;
}

function isActive() public view returns(bool) {
return state == State.Active;
}
}

結構體

使用 struct 關鍵字建立一個結構體。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
contract MyContract {
uint public peopleCount;

Person[] public people;

struct Person {
string firstName;
string lastName;
}

function addPerson(string memory _firstName, string memory _lastName) public {
peopleCount++;
people.push(Person(_firstName, _lastName));
}
}

字典

使用 mapping 關鍵字建立一個字典。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
contract MyContract {
uint public peopleCount;

mapping(uint => Person) public people;

struct Person {
string firstName;
string lastName;
}

function addPerson(string memory _firstName, string memory _lastName) public {
peopleCount++;
people[peopleCount] = Person(_firstName, _lastName);
}
}

可視性

public

使用 public 關鍵字,表示一個公開的方法。對於公開的變數,會自動產生一個 Getter 方法。

1
2
3
function increment() public {
value++;
}

internal

使用 internal 關鍵字,表示一個內部的方法,可以被當前或衍生的合約使用。

1
2
3
function increment() internal {
value++;
}

private

使用 private 關鍵字,表示一個私有的方法,僅能在當前的合約中使用。

1
2
3
function increment() private {
value++;
}

全域變數

  • block.timestamp:區塊時間戳。
  • msg.sender(address):訊息發送者(當前的呼叫)。
  • msg.value(uint):隨訊息發送的 wei 數量。
  • tx.origin(address): 交易發送者(完整的呼叫)。

函式修飾器

函式修飾器可以改變函式的行為。例如,它們可以在執行該函數之前自動檢查某些條件。需要使用 _; 符號來指示函式被注入的位置。

以下建立一個 onlyOwner 修飾器,要求只有初始化合約的人才可以執行該函式。

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
contract MyContract {
uint public peopleCount;

mapping(uint => Person) public people;

address owner;

modifier onlyOwner() {
require(msg.sender == owner);
_;
}

struct Person {
string firstName;
string lastName;
}

constructor() {
owner = msg.sender;
}

function addPerson(string memory _firstName, string memory _lastName) public onlyOwner {
incrementPeopleCount();
people[peopleCount] = Person(_firstName, _lastName);
}
}

以下建立一個 onlyWhileOpen 修飾器,要求只有在指定時間內才可以執行該函式。

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
contract MyContract {
uint public peopleCount;

mapping(uint => Person) public people;

uint openningTime = 1651931171;

modifier onlyWhileOpen() {
require(block.timestamp >= openningTime);
_;
}

struct Person {
string firstName;
string lastName;
}

function addPerson(string memory _firstName, string memory _lastName) public onlyWhileOpen {
incrementPeopleCount();
people[peopleCount] = Person(_firstName, _lastName);
}

function getPeopleCount() public view returns(uint) {
return peopleCount;
}

function incrementPeopleCount() internal {
peopleCount++;
}
}

交易

使用 payable 修飾器,讓函式可以交易以太幣。

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

pragma solidity ^0.8.0;

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

address payable wallet;

constructor(address payable _wallet) {
wallet = _wallet;
}

// send Ether to the wallet
function buyToken() public payable {
balances[msg.sender] += 1;
wallet.transfer(msg.value);
}
}

事件

使用 event 關鍵字,在特定函式執行時被觸發,使得前端應用程式可以監聽此事件。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
contract MyContract {
mapping(address => uint) public balances;

address payable wallet;

event Purchased(
address indexed buyer,
uint amount
);

constructor(address payable _wallet) {
wallet = _wallet;
}

function buyToken() public payable {
balances[msg.sender] += 1;
wallet.transfer(msg.value);

emit Purchased(msg.sender, 1);
}
}

繼承

一個檔案內可以有多個合約,如下。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
contract ERC20Token {
string public name;

mapping(address => uint) public balances;

function mint() public {
balances[tx.origin]++;
}
}

contract MyContract {
address payable wallet;
address public token;

constructor(address payable _wallet, address _token) {
wallet = _wallet;
token = _token;
}

function buyToken() public payable {
ERC20Token(address(token)).mint();
wallet.transfer(msg.value);
}
}

可以使用 is 關鍵字,讓子合約選擇要繼承的父合約。

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
contract ERC20Token {
string public name;

mapping(address => uint) public balances;

constructor(string memory _name) {
name = _name;
}

function mint() virtual public {
balances[tx.origin]++;
}
}

contract MyToken is ERC20Token {
string public symbol;

address[] public owners;

uint ownerCount;

constructor(string memory _name, string memory _symbol) ERC20Token(_name) {
symbol = _symbol;
}

function mint() override public {
super.mint();
ownerCount++;
owners.push(msg.sender);
}
}

函式庫

使用 library 關鍵字,建立函式庫。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
library Math {
function divide(uint a, uint b) internal pure returns(uint) {
require(b > 0);
return a / b;
}
}

contract MyContract {
uint public value;

function calculate(uint _value1, uint _value2) public {
value = Math.divide(_value1, _value2);
}
}

建立 Math.sol 檔,將函式抽離到單獨的檔案。

1
2
3
4
5
6
7
8
9
import "./Math.sol";

contract MyContract {
uint public value;

function calculate(uint _value1, uint _value2) public {
value = Math.divide(_value1, _value2);
}
}

MyContract.sol 檔中引入 Math 函式庫。

1
2
3
4
5
6
7
8
9
import "./Math.sol";

contract MyContract {
uint public value;

function calculate(uint _value1, uint _value2) public {
value = Math.divide(_value1, _value2);
}
}

擴充

openzeppelin-contracts 專案中的 SafeMath 函式庫為例,將程式碼複製到 SafeMath.sol 檔。

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
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
/**
* @dev Wrappers over Solidity's arithmetic operations.
*
* NOTE: `SafeMath` is generally not needed starting with Solidity 0.8, since the compiler
* now has built in overflow checking.
*/
library SafeMath {
/**
* @dev Returns the addition of two unsigned integers, with an overflow flag.
*
* _Available since v3.4._
*/
function tryAdd(uint256 a, uint256 b) internal pure returns (bool, uint256) {
unchecked {
uint256 c = a + b;
if (c < a) return (false, 0);
return (true, c);
}
}

/**
* @dev Returns the subtraction of two unsigned integers, with an overflow flag.
*
* _Available since v3.4._
*/
function trySub(uint256 a, uint256 b) internal pure returns (bool, uint256) {
unchecked {
if (b > a) return (false, 0);
return (true, a - b);
}
}

/**
* @dev Returns the multiplication of two unsigned integers, with an overflow flag.
*
* _Available since v3.4._
*/
function tryMul(uint256 a, uint256 b) internal pure returns (bool, uint256) {
unchecked {
// Gas optimization: this is cheaper than requiring 'a' not being zero, but the
// benefit is lost if 'b' is also tested.
// See: https://github.com/OpenZeppelin/openzeppelin-contracts/pull/522
if (a == 0) return (true, 0);
uint256 c = a * b;
if (c / a != b) return (false, 0);
return (true, c);
}
}

/**
* @dev Returns the division of two unsigned integers, with a division by zero flag.
*
* _Available since v3.4._
*/
function tryDiv(uint256 a, uint256 b) internal pure returns (bool, uint256) {
unchecked {
if (b == 0) return (false, 0);
return (true, a / b);
}
}

/**
* @dev Returns the remainder of dividing two unsigned integers, with a division by zero flag.
*
* _Available since v3.4._
*/
function tryMod(uint256 a, uint256 b) internal pure returns (bool, uint256) {
unchecked {
if (b == 0) return (false, 0);
return (true, a % b);
}
}

/**
* @dev Returns the addition of two unsigned integers, reverting on
* overflow.
*
* Counterpart to Solidity's `+` operator.
*
* Requirements:
*
* - Addition cannot overflow.
*/
function add(uint256 a, uint256 b) internal pure returns (uint256) {
return a + b;
}

/**
* @dev Returns the subtraction of two unsigned integers, reverting on
* overflow (when the result is negative).
*
* Counterpart to Solidity's `-` operator.
*
* Requirements:
*
* - Subtraction cannot overflow.
*/
function sub(uint256 a, uint256 b) internal pure returns (uint256) {
return a - b;
}

/**
* @dev Returns the multiplication of two unsigned integers, reverting on
* overflow.
*
* Counterpart to Solidity's `*` operator.
*
* Requirements:
*
* - Multiplication cannot overflow.
*/
function mul(uint256 a, uint256 b) internal pure returns (uint256) {
return a * b;
}

/**
* @dev Returns the integer division of two unsigned integers, reverting on
* division by zero. The result is rounded towards zero.
*
* Counterpart to Solidity's `/` operator.
*
* Requirements:
*
* - The divisor cannot be zero.
*/
function div(uint256 a, uint256 b) internal pure returns (uint256) {
return a / b;
}

/**
* @dev Returns the remainder of dividing two unsigned integers. (unsigned integer modulo),
* reverting when dividing by zero.
*
* Counterpart to Solidity's `%` operator. This function uses a `revert`
* opcode (which leaves remaining gas untouched) while Solidity uses an
* invalid opcode to revert (consuming all remaining gas).
*
* Requirements:
*
* - The divisor cannot be zero.
*/
function mod(uint256 a, uint256 b) internal pure returns (uint256) {
return a % b;
}

/**
* @dev Returns the subtraction of two unsigned integers, reverting with custom message on
* overflow (when the result is negative).
*
* CAUTION: This function is deprecated because it requires allocating memory for the error
* message unnecessarily. For custom revert reasons use {trySub}.
*
* Counterpart to Solidity's `-` operator.
*
* Requirements:
*
* - Subtraction cannot overflow.
*/
function sub(
uint256 a,
uint256 b,
string memory errorMessage
) internal pure returns (uint256) {
unchecked {
require(b <= a, errorMessage);
return a - b;
}
}

/**
* @dev Returns the integer division of two unsigned integers, reverting with custom message on
* division by zero. The result is rounded towards zero.
*
* Counterpart to Solidity's `/` operator. Note: this function uses a
* `revert` opcode (which leaves remaining gas untouched) while Solidity
* uses an invalid opcode to revert (consuming all remaining gas).
*
* Requirements:
*
* - The divisor cannot be zero.
*/
function div(
uint256 a,
uint256 b,
string memory errorMessage
) internal pure returns (uint256) {
unchecked {
require(b > 0, errorMessage);
return a / b;
}
}

/**
* @dev Returns the remainder of dividing two unsigned integers. (unsigned integer modulo),
* reverting with custom message when dividing by zero.
*
* CAUTION: This function is deprecated because it requires allocating memory for the error
* message unnecessarily. For custom revert reasons use {tryMod}.
*
* Counterpart to Solidity's `%` operator. This function uses a `revert`
* opcode (which leaves remaining gas untouched) while Solidity uses an
* invalid opcode to revert (consuming all remaining gas).
*
* Requirements:
*
* - The divisor cannot be zero.
*/
function mod(
uint256 a,
uint256 b,
string memory errorMessage
) internal pure returns (uint256) {
unchecked {
require(b > 0, errorMessage);
return a % b;
}
}
}

MyContract.sol 檔中引入 SafeMath 函式庫。並使用 using...for... 語法,將 SafeMath 的函式擴充到 uint 型別。若該函式本身有參數的話,預設是以合約中呼叫該函式的變數作為第一個參數。

1
2
3
4
5
6
7
8
9
10
11
import "./SafeMath.sol";

contract MyContract {
using SafeMath for uint;

uint public value;

function calculate(uint _value1, uint _value2) public {
value = _value1.div(_value2);
}
}

參考資料