1. 变量
在solidity中共有3种类型的变量:
- local变量
- 在函数内部声明
- 不会存储在区块链上
- state变量
- 在函数外部声明
- 存储在区块链上
- global变量
- 提供区块链全局信息的变量,如
msg.sender、block.timestamp等
- 提供区块链全局信息的变量,如
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.13;
contract Variables {
// State variables are stored on the blockchain.
string public text = "Hello";
uint public num = 123;
function doSomething() public {
// Local variables are not saved to the blockchain.
uint i = 456;
// Here are some global variables
uint timestamp = block.timestamp; // Current block timestamp
address sender = msg.sender; // address of the caller
}
}
常量
还有一种常量类型也很常见,用来表示不可以被修改的变量。按照约定常量通常使用大写字母来表示。合理使用常量可以减少gas费的消耗。
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.13;
contract Constants {
// coding convention to uppercase constant variables
address public constant MY_ADDRESS = 0x777788889999AaAAbBbbCcccddDdeeeEfFFfCcCc;
uint public constant MY_UINT = 123;
}
Immutable 类型的变量
和常量类似,Immutable类型的值可以在constructor中被赋值,但是后面不可以再修改了。
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.13;
contract Immutable {
// coding convention to uppercase constant variables
address public immutable MY_ADDRESS;
uint public immutable MY_UINT;
constructor(uint _myUint) {
MY_ADDRESS = msg.sender;
MY_UINT = _myUint;
}
}
2. 读写一个状态变量 state variable
给state变量赋值、或者更新操作,需要通过发送交易来进行;
但是,读取state变量就不需要发起交易和支付手续费,可以直接读取。
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.13;
contract SimpleStorage {
// State variable to store a number
uint public num;
// You need to send a transaction to write to a state variable.
function set(uint _num) public {
num = _num;
}
// You can read from a state variable without sending a transaction.
function get() public view returns (uint) {
return num;
}
}
3. 单位 Ether 和 Wei
EVM中的交易需要通过ether来支付费用。就像1元=100分一样, 1 ether=10^18wei, 1 Gwei=10^9 wei
Ether的用途
- 支付区块奖励
- 支付交易费用
- 转账手续费
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.13;
contract EtherUnits {
uint public oneWei = 1 wei;
// 1 wei is equal to 1
bool public isOneWei = 1 wei == 1;
uint public oneEther = 1 ether;
// 1 ether is equal to 10^18 wei
bool public isOneEther = 1 ether == 1e18;
}
4. Gas
gas表示一个计算单元gas spent表示在一个tx中使用的gas数量gas price表示你愿意为每个gas支付多少ether
所以,在每个交易tx中,总的花费(ether)=gas price * gas spent
gas price高的具有优先打包交易的权利。没有花费完的gas会退款。
4.1 Gas limit
花费gas时,一共有2种上限:
gas limit:每个tx中你愿意支付的最大数量的gas费,由你来指定;block gas limit:在每个区块中允许的gas费最大值,由network来指定;
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.13;
contract Gas {
uint public i = 0;
// Using up all of the gas that you send causes your transaction to fail.
// State changes are undone.
// Gas spent are not refunded.
function forever() public {
// Here we run a loop until all of the gas are spent
// and the transaction fails
while (true) {
i += 1;
}
}
}
5. If / Else
Solidity 支持 if, else if 和 else 的条件语句。
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.13;
contract IfElse {
function foo(uint x) public pure returns (uint) {
if (x < 10) {
return 0;
} else if (x < 20) {
return 1;
} else {
return 2;
}
}
function ternary(uint _x) public pure returns (uint) {
// if (_x < 10) {
// return 1;
// }
// return 2;
// shorthand way to write if / else statement
// the "?" operator is called the ternary operator
return _x < 10 ? 1 : 2;
}
}
6. for 和 while 循环
Solidity 支持 for、while 和 do while 循环。
不要写没有边界的循环(死循环),因为这样会导致程序达到gas limit的上限,从而会导致整个交易tx失败。
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.13;
contract Loop {
function loop() public {
// for loop
for (uint i = 0; i < 10; i++) {
if (i == 3) {
// Skip to next iteration with continue
continue;
}
if (i == 5) {
// Exit loop with break
break;
}
}
// while loop
uint j;
while (j < 10) {
j++;
}
}
}
7. Mapping
Mapping 数据结构用这个语法来表示:mapping(keyType => valueType)。
keyType可以是任意内置值类型、bytes、string或者合约类型valueType:可以是任意类型,包括另一个mapping或者数组
mapping不可以迭代。
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.13;
contract Mapping {
// Mapping from address to uint
mapping(address => uint) public myMap;
function get(address _addr) public view returns (uint) {
// Mapping always returns a value.
// If the value was never set, it will return the default value.
return myMap[_addr];
}
function set(address _addr, uint _i) public {
// Update the value at this address
myMap[_addr] = _i;
}
function remove(address _addr) public {
// Reset the value to the default value.
delete myMap[_addr];
}
}
contract NestedMapping {
// Nested mapping (mapping from address to another mapping)
mapping(address => mapping(uint => bool)) public nested;
function get(address _addr1, uint _i) public view returns (bool) {
// You can get values from a nested mapping
// even when it is not initialized
return nested[_addr1][_i];
}
function set(
address _addr1,
uint _i,
bool _boo
) public {
nested[_addr1][_i] = _boo;
}
function remove(address _addr1, uint _i) public {
delete nested[_addr1][_i];
}
}
8. Array
数组即可以是定长的(compile-time fixed),也可以是动态长度的(dynamic size)。
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.13;
contract Array {
// Several ways to initialize an array
uint[] public arr;
uint[] public arr2 = [1, 2, 3];
// Fixed sized array, all elements initialize to 0
uint[10] public myFixedSizeArr;
function get(uint i) public view returns (uint) {
return arr[i];
}
// Solidity can return the entire array.
// But this function should be avoided for
// arrays that can grow indefinitely in length.
function getArr() public view returns (uint[] memory) {
return arr;
}
function push(uint i) public {
// Append to array
// This will increase the array length by 1.
arr.push(i);
}
function pop() public {
// Remove last element from array
// This will decrease the array length by 1
arr.pop();
}
function getLength() public view returns (uint) {
return arr.length;
}
function remove(uint index) public {
// Delete does not change the array length.
// It resets the value at index to it's default value,
// in this case 0
delete arr[index];
}
function examples() external {
// create array in memory, only fixed size can be created
uint[] memory a = new uint[](5);
}
}
9. Enum
Solidity 支持枚举类型。枚举类型对于建模选择和跟踪状态很有用。
枚举可以在合约外部声明。
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.13;
contract Enum {
// Enum representing shipping status
enum Status {
Pending,
Shipped,
Accepted,
Rejected,
Canceled
}
// Default value is the first element listed in
// definition of the type, in this case "Pending"
Status public status;
// Returns uint
// Pending - 0
// Shipped - 1
// Accepted - 2
// Rejected - 3
// Canceled - 4
function get() public view returns (Status) {
return status;
}
// Update status by passing uint into input
function set(Status _status) public {
status = _status;
}
// You can update to a specific enum like this
function cancel() public {
status = Status.Canceled;
}
// delete resets the enum to its first value, 0
function reset() public {
delete status;
}
}
9.1 声明和导入 declaring and importing Enum
在一个独立文件中声明:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.13;
// This is saved 'EnumDeclaration.sol'
enum Status {
Pending,
Shipped,
Accepted,
Rejected,
Canceled
}
在另一个文件中导入:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.13;
import "./EnumDeclaration.sol";
contract Enum {
Status public status;
}
10. Structs 结构体
通过 structs 关键字可以定义你自己的类型。struct 用来把相关的数据组合起来。
struct 可以声明在 contract 外部,也可以在另外一个合约代码中被导入。
用最经典的todos举个例子:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.13;
contract Todos {
struct Todo {
string text;
bool completed;
}
// An array of 'Todo' structs
Todo[] public todos;
function create(string calldata _text) public {
// 3 ways to initialize a struct
// - calling it like a function
todos.push(Todo(_text, false));
// key value mapping
todos.push(Todo({text: _text, completed: false}));
// initialize an empty struct and then update it
Todo memory todo;
todo.text = _text;
// todo.completed initialized to false
todos.push(todo);
}
// Solidity automatically created a getter for 'todos' so
// you don't actually need this function.
function get(uint _index) public view returns (string memory text, bool completed) {
Todo storage todo = todos[_index];
return (todo.text, todo.completed);
}
// update text
function updateText(uint _index, string calldata _text) public {
Todo storage todo = todos[_index];
todo.text = _text;
}
// update completed
function toggleCompleted(uint _index) public {
Todo storage todo = todos[_index];
todo.completed = !todo.completed;
}
}
10.1 声明和导入
在一个独立文件中声明:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.13;
// This is saved 'StructDeclaration.sol'
struct Todo {
string text;
bool completed;
}
在另一个文件中导入:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.13;
import "./StructDeclaration.sol";
contract Todos {
// An array of 'Todo' structs
Todo[] public todos;
}
11. 数据存在的位置 - Storage,Memory 和 Calldata
变量在声明时通过关键字 storage, memory 或 calldata 来显式地制定数据存放的位置。
它们的区别是:
storage变量是state variable,保存在区块上;memory:变量在内存中,当函数被调用时才存在;calldata:一种特殊的数据位置,它包含方法的参数。
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.13;
contract DataLocations {
uint[] public arr;
mapping(uint => address) map;
struct MyStruct {
uint foo;
}
mapping(uint => MyStruct) myStructs;
function f() public {
// call _f with state variables
_f(arr, map, myStructs[1]);
// get a struct from a mapping
MyStruct storage myStruct = myStructs[1];
// create a struct in memory
MyStruct memory myMemStruct = MyStruct(0);
}
function _f(
uint[] storage _arr,
mapping(uint => address) storage _map,
MyStruct storage _myStruct
) internal {
// do something with storage variables
}
// You can return memory variables
function g(uint[] memory _arr) public returns (uint[] memory) {
// do something with memory array
}
function h(uint[] calldata _arr) external {
// do something with calldata array
}
}
12. Function 函数
这一点和JS布他一样,public function 不能使用某些类型作为输入或输出。比如:
map既不能用户输入,也不能用于输出- 数组作为输入参数;
- 数组也可以作为输出参数
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.13;
contract Function {
// Functions can return multiple values.
function returnMany()
public
pure
returns (
uint,
bool,
uint
)
{
return (1, true, 2);
}
// Return values can be named.
function named()
public
pure
returns (
uint x,
bool b,
uint y
)
{
return (1, true, 2);
}
// Return values can be assigned to their name.
// In this case the return statement can be omitted.
function assigned()
public
pure
returns (
uint x,
bool b,
uint y
)
{
x = 1;
b = true;
y = 2;
}
// Use destructuring assignment when calling another
// function that returns multiple values.
function destructuringAssignments()
public
pure
returns (
uint,
bool,
uint,
uint,
uint
)
{
(uint i, bool b, uint j) = returnMany();
// Values can be left out.
(uint x, , uint y) = (4, 5, 6);
return (i, b, j, x, y);
}
// Cannot use map for either input or output
// Can use array for input
function arrayInput(uint[] memory _arr) public {}
// Can use array for output
uint[] public arr;
function arrayOutput() public view returns (uint[] memory) {
return arr;
}
}
12.1 View 和 Pure function
Getter functions 可以使用 view 和 pure 关键字来声明。
View 表示执行该方法时不会发生状态变化;
Pure 表示执行该方法时没有状态变量发生变化或者读取状态变量。
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.13;
contract ViewAndPure {
uint public x = 1;
// Promise not to modify the state.
function addToX(uint y) public view returns (uint) {
return x + y;
}
// Promise not to modify or read from the state.
function add(uint i, uint j) public pure returns (uint) {
return i + j;
}
}
12.2 Function Modifier 函数修饰符
修饰符不仅仅是修饰函数的,修饰符还是一段代码,可以在函数调用前或调用后被执行。
修饰符的作用:
- 限制访问
- 验证输入
- 防范重入黑客
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.13;
contract FunctionModifier {
// We will use these variables to demonstrate how to use
// modifiers.
address public owner;
uint public x = 10;
bool public locked;
constructor() {
// Set the transaction sender as the owner of the contract.
owner = msg.sender;
}
// Modifier to check that the caller is the owner of
// the contract.
modifier onlyOwner() {
require(msg.sender == owner, "Not owner");
// Underscore is a special character only used inside
// a function modifier and it tells Solidity to
// execute the rest of the code.
_;
}
// Modifiers can take inputs. This modifier checks that the
// address passed in is not the zero address.
modifier validAddress(address _addr) {
require(_addr != address(0), "Not valid address");
_;
}
function changeOwner(address _newOwner) public onlyOwner validAddress(_newOwner) {
owner = _newOwner;
}
// Modifiers can be called before and / or after a function.
// This modifier prevents a function from being called while
// it is still executing.
modifier noReentrancy() {
require(!locked, "No reentrancy");
locked = true;
_;
locked = false;
}
function decrement(uint i) public noReentrancy {
x -= i;
if (i > 1) {
decrement(i - 1);
}
}
}
13 Error
Error会撤销在交易tx中的所有状态变更。
可以通过调用require, revert 或 assert来抛出错误:
require用来在执行前验证输入和条件的;revert和require类似,具体区别看下面例子;assert用来检查代码永远不能是false。一个失败的断言意味着那里有错误。
可以使用自定义错误来节省gas费。
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.13;
contract Error {
function testRequire(uint _i) public pure {
// Require should be used to validate conditions such as:
// - inputs
// - conditions before execution
// - return values from calls to other functions
require(_i > 10, "Input must be greater than 10");
}
function testRevert(uint _i) public pure {
// Revert is useful when the condition to check is complex.
// This code does the exact same thing as the example above
if (_i <= 10) {
revert("Input must be greater than 10");
}
}
uint public num;
function testAssert() public view {
// Assert should only be used to test for internal errors,
// and to check invariants.
// Here we assert that num is always equal to 0
// since it is impossible to update the value of num
assert(num == 0);
}
// custom error
error InsufficientBalance(uint balance, uint withdrawAmount);
function testCustomError(uint _withdrawAmount) public view {
uint bal = address(this).balance;
if (bal < _withdrawAmount) {
revert InsufficientBalance({balance: bal, withdrawAmount: _withdrawAmount});
}
}
}
再来个例子:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.13;
contract Account {
uint public balance;
uint public constant MAX_UINT = 2**256 - 1;
function deposit(uint _amount) public {
uint oldBalance = balance;
uint newBalance = balance + _amount;
// balance + _amount does not overflow if balance + _amount >= balance
require(newBalance >= oldBalance, "Overflow");
balance = newBalance;
assert(balance >= oldBalance);
}
function withdraw(uint _amount) public {
uint oldBalance = balance;
// balance - _amount does not underflow if balance >= _amount
require(balance >= _amount, "Underflow");
if (balance < _amount) {
revert("Underflow");
}
balance -= _amount;
assert(balance <= oldBalance);
}
}
14. Events
事件可以记录以太坊区块链的信息,作用有:
- 监听事件并更新UI
- 是一种便宜的存储形式
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.13;
contract Event {
// Event declaration
// Up to 3 parameters can be indexed.
// Indexed parameters helps you filter the logs by the indexed parameter
event Log(address indexed sender, string message);
event AnotherLog();
function test() public {
emit Log(msg.sender, "Hello World!");
emit Log(msg.sender, "Hello EVM!");
emit AnotherLog();
}
}
15. Constructor
constructor是一个可选的函数,通常在创建合约时执行。
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.13;
// Base contract X
contract X {
string public name;
constructor(string memory _name) {
name = _name;
}
}
// Base contract Y
contract Y {
string public text;
constructor(string memory _text) {
text = _text;
}
}
// There are 2 ways to initialize parent contract with parameters.
// Pass the parameters here in the inheritance list.
contract B is X("Input to X"), Y("Input to Y") {
}
contract C is X, Y {
// Pass the parameters here in the constructor,
// similar to function modifiers.
constructor(string memory _name, string memory _text) X(_name) Y(_text) {}
}
// Parent constructors are always called in the order of inheritance
// regardless of the order of parent contracts listed in the
// constructor of the child contract.
// Order of constructors called:
// 1. X
// 2. Y
// 3. D
contract D is X, Y {
constructor() X("X was called") Y("Y was called") {}
}
// Order of constructors called:
// 1. X
// 2. Y
// 3. E
contract E is X, Y {
constructor() Y("Y was called") X("X was called") {}
}
16. Inheritance 继承
Solidity支持多继承。合约继承另一个合约通过关键字is实现。
将被子合约复写的函数必须声明为virtual。
将复写父合约的函数必须声明为override。
继承的顺序很重要。
你必须按照从“最基础”到“最衍生”的顺序列出父合约。
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.13;
/* Graph of inheritance
A
/ \
B C
/ \ /
F D,E
*/
contract A {
function foo() public pure virtual returns (string memory) {
return "A";
}
}
// Contracts inherit other contracts by using the keyword 'is'.
contract B is A {
// Override A.foo()
function foo() public pure virtual override returns (string memory) {
return "B";
}
}
contract C is A {
// Override A.foo()
function foo() public pure virtual override returns (string memory) {
return "C";
}
}
// Contracts can inherit from multiple parent contracts.
// When a function is called that is defined multiple times in
// different contracts, parent contracts are searched from
// right to left, and in depth-first manner.
contract D is B, C {
// D.foo() returns "C"
// since C is the right most parent contract with function foo()
function foo() public pure override(B, C) returns (string memory) {
return super.foo();
}
}
contract E is C, B {
// E.foo() returns "B"
// since B is the right most parent contract with function foo()
function foo() public pure override(C, B) returns (string memory) {
return super.foo();
}
}
// Inheritance must be ordered from “most base-like” to “most derived”.
// Swapping the order of A and B will throw a compilation error.
contract F is A, B {
function foo() public pure override(A, B) returns (string memory) {
return super.foo();
}
}
17. Interface 接口
通过声名interface和其他合约交互。
接口
- 不可以有任何实现的方法;
- 可以从另一个
interface继承; - 所有声明的函数必须是
external的; - 不能声明构造函数;
- 不能声明状态变量
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.13;
contract Counter {
uint public count;
function increment() external {
count += 1;
}
}
interface ICounter {
function count() external view returns (uint);
function increment() external;
}
contract MyContract {
function incrementCounter(address _counter) external {
ICounter(_counter).increment();
}
function getCount(address _counter) external view returns (uint) {
return ICounter(_counter).count();
}
}
// Uniswap example
interface UniswapV2Factory {
function getPair(address tokenA, address tokenB)
external
view
returns (address pair);
}
interface UniswapV2Pair {
function getReserves()
external
view
returns (
uint112 reserve0,
uint112 reserve1,
uint32 blockTimestampLast
);
}
contract UniswapExample {
address private factory = 0x5C69bEe701ef814a2B6a3EDD4B1652CB9cc5aA6f;
address private dai = 0x6B175474E89094C44Da98b954EedeAC495271d0F;
address private weth = 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2;
function getTokenReserves() external view returns (uint, uint) {
address pair = UniswapV2Factory(factory).getPair(dai, weth);
(uint reserve0, uint reserve1, ) = UniswapV2Pair(pair).getReserves();
return (reserve0, reserve1);
}
}
to be continued...