目录
- 1 Hello World
- 2 第一个Dapp
- 3 基础数据类型
- 4 变量variables
- 5 常量constant
- 6 不可变量immutable
- 7 读写状态变量
- 8 ether和wei
- 9 gas和gasprice
- 10 if else
- 11 for and while
- 12 bytes和string
- 13 Mapping
- 14 array数组
- 15 枚举Enum
- 16 结构体Struct
- 17 存储位置-memory、storage、calldata
- 18 函数Function
- 19 view和pure
- 20 错误Error
- 21 修饰器modifier
- 22 事件Event
- 23 Inheritance、virtual、override
- 24 继承的状态变量覆盖(shadowing)
- 25 构造函数constructor
- 26 可见性visibility
- 27 abstract
- 28 Interface
- 29 library
- 30 传递结构体


Hello World
pragma solidity ^0.8.13; 表明当前编译合约的版本号,向上兼容至0.9
```js
// SPDX-License-Identifier: MIT
// compiler version must be greater than or equal to 0.8.13 and less than 0.9.0
pragma solidity ^0.8.13;
contract HelloWorld {
string public greet = "Hello World!";
}
Try on Remix
第一个Dapp
// 指定编译器版本,版本标识符
pragma solidity ^0.8.13;
// 关键字 contract 跟java的class一样 智能合约是Inbox
contract Inbox{
// 状态变量,存在链上
string public message;
// 构造函数
constructor(string memory initMessage) {
// 本地变量
string memory tmp = initMessage;
message = tmp;
}
// 写操作,需要支付手续费
function setMessage(string memory _newMessage) public {
message = _newMessage;
}
// 读操作,不需要支付手续费
function getMessage() public view returns(string memory) {
return message;
}
}
Try on Remix
基础数据类型
- int(有符号整型,有正有负)int默认为int256
- uint(无符号整型,无负数)uint默认为uint256
- 以8位为区间,支持int8,int16,int24 至 int256(uint同理)
- bool类型:true,false
- 定长字节:bytes1~bytes32
- 地址:address(20个字节,40个16进制字符,共160位),如:0x5B38Da6a701c568545dCfcB03FcB875f56beddC4
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.13;
contract Primitives {
bool public flag = true;
/*
uint stands for unsigned integer, meaning non negative integers
different sizes are available
uint8 ranges from 0 to 2 ** 8 - 1
uint16 ranges from 0 to 2 ** 16 - 1
...
uint256 ranges from 0 to 2 ** 256 - 1
*/
uint8 public u8 = 1;
uint public u256 = 456;
uint public u = 123; // uint is an alias for uint256
/*
Negative numbers are allowed for int types.
Like uint, different ranges are available from int8 to int256
int256 ranges from -2 ** 255 to 2 ** 255 - 1
int128 ranges from -2 ** 127 to 2 ** 127 - 1
*/
int8 public i8 = -1;
int public i256 = 456;
int public i = -123; // int is same as int256
// minimum and maximum of int
int public minInt = type(int).min;
int public maxInt = type(int).max;
address public addr = 0xCA35b7d915458EF540aDe6068dFe2F44E8fa733c;
/*
In Solidity, the data type byte represent a sequence of bytes.
Solidity presents two type of bytes types :
- fixed-sized byte arrays
- dynamically-sized byte arrays.
The term bytes in Solidity represents a dynamic array of bytes.
It’s a shorthand for byte[] .
*/
bytes1 a = 0xb5; // [10110101]
bytes1 b = 0x56; // [01010110]
// Default values
// Unassigned variables have a default value
bool public defaultBoo; // false
uint public defaultUint; // 0
int public defaultInt; // 0
address public defaultAddr; // 0x0000000000000000000000000000000000000000
}
Try on Remix
变量variables
以太坊一共有三种类型的变量
1.状态变量(state)
- 定义在合约内,函数外
- 存储在链上
3.本地变量(local)
- 定义在函数内
- 不会存储在链上
3.全局变量(global)
- 与当前合约无关,描述整个区块链的信息(时间,块高等)
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.13;
contract Variables {
// State variables are stored on the blockchain.
string public msg = "Hello";
uint public age = 26;
function test() public {
// Local variables are not saved to the blockchain.
uint i = 456;
// Here are some global variables
uint height = block.blocks; // Current block height
address sender = msg.sender; // address of the caller
}
}
Try on Remix
常量constant
- 常量与变量相对,需要硬编码在合约中,合约部署之后,无法改变。
- 常量更加节约gas,一般用大写来代表常量。
- 高阶用法:clone合约时,如果合约内有初始值,必须使用constant,否则clone的新合约初始值为空值。
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.13;
contract Constants {
// coding convention to uppercase constant variables
address public constant MY_ADDRESS = 0x194d8e0c85f7826d828E1d2E9BCe714064F4f8D7;
uint public constant MY_UINT = 123;
}
Try on Remix
不可变量immutable
与常量类似,但是不必硬编码,可以在构造函数时传值,部署后无法改变。
// 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;
}
}
Try on Remix
读写状态变量
- 写状态变量(上链)是一笔交易(tx),需要矿工打包,所以需要花费资金;
- 读取状态变量,是从账本中获取数据,不是一笔交易,所以免费。(必须加上view)
// 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;
}
}
Try on Remix
ether和wei
- 常用单位为:wei,gwei,ether
- 不含任何后缀的默认单位是 wei
- 1 gwei = 10^9 wei
// 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;
}
Try on Remix
gas和gasprice


gas描述执行一笔交易时需要花费多少ether!(1 ether = 10^18wei)
交易手续费 = gas_used * gas_price,其中:
- gas:是数量单位,uint
- gas_used:表示一笔交易实际消耗的gas数量
- gas_price:每个gas的价格,单位是wei或gwei
- gas limit:表示你允许这一笔交易消耗的gas上限,用户自己设置(防止因为bug导致的损失)
- 如果gas_used小于gas_limit,剩余gas会返回给用户,这个值不再合约层面设置,在交易层面设置(如metamask)
- 如果gas_used大于gas_limit,交易失败,资金不退回
- block gas limit:表示一个区块能够允许的最大gas数量,由区块链网络设置


验证:
gas_used: 197083
gas_price: 0.000000078489891145
cost = gas_used * gas_price = 197083 * 0.000000078489891145 = 0.015469023216530034,#与上图一致
Try on Remix
if else
支持常规的if , else if, else
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
return _x < 10 ? 1 : 2;
}
}
Try on Remix
for and while
solidity支持for, while, do while循环;
尽量不要使用没有边界的循环,因为会导致达到gas limit,进而导致交易执行失败,因此很少使用while和do while
// 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++;
}
}
}
Try on Remix
bytes和string
byteN、bytes、string直接的关系:


bytes:
- bytes是动态数组,相当于byte数组(如:byte[10])
- 支持push方法添加
- 可以与string相互转换
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.13;
contract Bytes {
bytes public name;
//1. 获取字节长度
function getLen() public view returns(uint256) {
return name.length;
}
//2. 可以不分空间,直接进行字符串赋值,会自动分配空间
function setValue(bytes memory input) public {
name = input;
}
//3. 支持push操作,在bytes最后面追加元素
function pushData() public {
name.push("h");
}
}
string:
- string 动态尺寸的UTF-8编码字符串,是特殊的可变字节数组
- string 不支持下标索引、不支持length、push方法
- string 可以修改(需通过bytes转换)
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.13;
contract String {
string public name = "lily";
function setName() public {
bytes(name)[0] = "L";
}
function getLength() public view returns(uint256) {
return bytes(name).length;
}
}
Mapping
- 定义:mapping(keyType => valueType) myMapping
- key可以是任意类型,value可以是任意类型(value也可以是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];
}
}
Try on Remix
array数组
- 定长数组(编译时确定)和动态数组
- 下面的arr和arr2是相同的,arr2多了三个初始化的值,arr2也支持push和pop操作
- 2022年,数组可以直接在构造函数中赋值
- new uint256 语法用于对memory进行修饰,storage不需要使用new
- 仅状态变量数组和storage支持动态扩容:push
// 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() pure external returns(uint[] memory) {
// create array in memory, only fixed size can be created
uint[] memory a = new uint[](5);
return a;
}
}
demo1:删除某个位置的元素,剩余元素向左移动(保留顺序)
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.13;
contract ArrayRemoveByShifting {
// [1, 2, 3] -- remove(1) --> [1, 3, 3] --> [1, 3]
// [1, 2, 3, 4, 5, 6] -- remove(2) --> [1, 2, 4, 5, 6, 6] --> [1, 2, 4, 5, 6]
// [1, 2, 3, 4, 5, 6] -- remove(0) --> [2, 3, 4, 5, 6, 6] --> [2, 3, 4, 5, 6]
// [1] -- remove(0) --> [1] --> []
uint[] public arr;
function remove(uint _index) public {
require(_index < arr.length, "index out of bound");
for (uint i = _index; i < arr.length - 1; i++) {
arr[i] = arr[i + 1];
}
arr.pop();
}
function test() external {
arr = [1, 2, 3, 4, 5];
remove(2);
// [1, 2, 4, 5]
assert(arr[0] == 1);
assert(arr[1] == 2);
assert(arr[2] == 4);
assert(arr[3] == 5);
assert(arr.length == 4);
arr = [1];
remove(0);
// []
assert(arr.length == 0);
}
}
demo2:删除某位置元素,使用最后一个元素填充(不考虑顺序)
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.13;
contract ArrayReplaceFromEnd {
uint[] public arr;
// Deleting an element creates a gap in the array.
// One trick to keep the array compact is to
// move the last element into the place to delete.
function remove(uint index) public {
// Move the last element into the place to delete
arr[index] = arr[arr.length - 1];
// Remove the last element
arr.pop();
}
function test() public {
arr = [1, 2, 3, 4];
remove(1);
// [1, 4, 3]
assert(arr.length == 3);
assert(arr[0] == 1);
assert(arr[1] == 4);
assert(arr[2] == 3);
remove(2);
// [1, 4]
assert(arr.length == 2);
assert(arr[0] == 1);
assert(arr[1] == 4);
}
}
Try on Remix
枚举Enum
枚举可以避免魔数,让程序更加易读,更好的进行状态管理,默认第一个值是:0
// 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;
}
}
Try on Remix
结构体Struct
自定义结构类型,将不同的数据类型组合到一个结构中,目前支持参数传递结构体。
枚举和结构体都可以定义在另外一个文件中,进行import后使用
// 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 memory _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 update(uint _index, string memory _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;
}
}
在外面定义结构体:StructDeclaration.sol(说明并不是所有数据都必须写在合约之内的)
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.13;
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;
}
避坑指南
- 传递结构体时,需要使用方括号包裹,并且里面有地址的时候,需要使用双引号包裹,否则失败
- 传递结构体数组时,需要在外层再次包裹方括号即可。
- 示例: 函数原型:
function createOffer(Token[] memory _tokens, GeneralInfo memory _general)
结构定义:
enum TokenType {
ERC20,
ERC721,
ERC1155
}
struct Token {
TokenType tokenType;
address tokenAddr;
uint256 tokenAmounts;
uint256 tokenId;
uint256 tokenPrice;
}
struct GeneralInfo {
address loanToken; //token to borrow
uint256 ltv; //8000 means 80%
bool featuredFlag; //true, false
uint256 loanAmount; //amounts to borrow
uint256 interestRate; //1100 means 11%
uint256 collateralThreshold; //8000 means 80%
uint256 repaymentDate; //timestamp + 30days
uint256 offerAvailable; //timestamp + 7days
}
正确传递参数方式如下:
tokens: [[0, '0x749B1c911170A5aFEb68d4B278cD5405C718fc7F',1000,0,0]],
general: ["0x749B1c911170A5aFEb68d4B278cD5405C718fc7F", 8000, false, 1000, 1100, 8000, 10000, 10000]
Try on Remix
存储位置-memory、storage、calldata
solidity中的存储位置分为三种,使用memory、storage、calldata来进行区分:
- storage:属于状态变量,数据会存储在链上,仅适用于所有引用类型:string,bytes,数组,结构体,mapping等;
- memory:仅存储在内存中,供函数使用,数据不上链,适用于所有类型,包括:
- 值类型(int,bool,bytes8等)
- 引用类型(string,bytes,数组,结构体,mapping)
- calldata:存储函数的参数的位置,是只读的(只有calldata支持数组切片,状态变量不可以直接使用切片,需要new新数组,然后使用for循环解决)
- 其他:Solidity 变量中 memory 、calldata 2 个表示作用非常类似,都是函数内部临时变量,它们最大的区别就是 calldata 是不可修改的,在某些只读的情况比较省 Gas.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.13;
contract DataLocations {
uint[] public arr = [1, 2, 3];
mapping(uint => address) public map;
struct MyStruct {
uint foo;
}
mapping(uint => MyStruct) public myStructs;
function test() public {
// get a struct from a mapping
MyStruct storage myStruct = myStructs[1];
// create a struct in memory
MyStruct memory myMemStruct = MyStruct(0);
// call _f with state variables
_f(arr, map, myStruct);
//invalid convertion, failed to call
// _f(arr, map, myMemStruct);
_g(arr);
this._h(arr);
}
function _f(
uint[] storage _arr,
mapping(uint => address) storage _map,
MyStruct storage _myStruct
) internal {
// do something with storage variables
_arr.push(100);
_map[20] = 0x5B38Da6a701c568545dCfcB03FcB875f56beddC4;
_myStruct.foo = 20;
}
// You can return memory variables
function _g(uint[] memory _arr) public returns (uint[] memory) {
// do something with memory array
_arr[0] = 100;
}
function _h(uint[] calldata _arr) external {
// do something with calldata array
// calldata is read-only
// _arr[2] = 200;
}
}
Try on Remix
函数Function
- 可以返回多个数据,使用()包裹起来即可
- 可以对返回值命名,此时可以省略return关键字
- 可以解构返回值,对忽略的返回值直接使用逗号留空
- 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;
}
}
Try on Remix
view和pure
view和pure用于修饰Getter函数(只读取数据的函数),其中:
- 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;
}
}
Try on Remix
错误Error
合约中发生错误时,整个交易状态都会进行回滚,一共有三个错误处理方式,具体如下:
- require:一般用于参数有效性校验,最常用。消耗的gas不会退回,剩余的gas退回;
- revert:与require类似,适用于校验条件复杂时使用;
- assert:用于断言绝对不改出错的地方,注意:
- 一般用于程序异常处理,触发了assert意味着存在bug;
- 不提供错误信息;
0.8.0之前,Asset会消耗掉所有提供的gaslimit,剩余的gas也不会返回。 (0.8.0之后已经不会再消耗了)
其他相关:
- 也可以自定义error,可以节约gas
- 错误消息的长度会影响:
- gas消耗数量
- 单个合约的大小
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.13;
contract Error {
function testRequire(uint _i) public pure {
// 期待_i > 10,如果i <= 10,则会抛出错误
require(_i > 10, "Input must be greater than 10");
}
function testRevert(uint _i) public pure {
// 如果校验条件过于复杂,则可以使用revert
if (_i <= 10) {
revert("Input must be greater than 10");
}
}
uint public num;
function testAssert() public view {
// assert用于校验不可变量,一般用于校验内部错误
// num == 0 为true时继续向下执行
// 不提供错误信息
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);
}
}
Try on Remix
修饰器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;
}
// 1. 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.
_;
}
// 2. 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);
}
}
}
Try on Remix
事件Event
事件是区块链上的日志,每当用户发起操作的时候,可以发送相应的事件,常用于:
- 监听用户对合约的调用
- 便宜的存储(用合约存储更加昂贵)
通过链下程序(如:subgraph)对合约进行事件监听,可以对Event进行搜集整理,从而做好数据统计,常用方式:
- 合约触发后发送事件
- subgraph对合约事件进行监听,计算(如:统计用户数量)
- 前端程序直接访问subgraph的服务,获得统计数据(这避免了在合约层面统计数据的费用,并且获取速度更快)
// 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();
}
}
当使用indexed关键字时:
- 如果是值类型的,则直接进行encode
- 如果是非值类型,如:array,string等,则使用keccak256哈希值
参考:
- https://docs.soliditylang.org/en/v0.8.13/abi-spec.html#indexed-event-encoding
- https://docs.soliditylang.org/en/v0.8.13/abi-spec.html#abi-events
Try on Remix
Inheritance、virtual、override
- 合约之间存在继承关系,使用关键字:is
- 如果父合约的方法想被子合约继承,则需要使用关键字:virtual
- 如果子合约想覆盖父合约的方法,则需要使用关键字:override
- 在子合约中如果想调用父合约的方法,需要使用关键字:super
- 继承的顺序很重要,遵循最远继承,即后面继承的合约会覆盖前面父合约的方法
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.10;
/* Inheritance tree
A
/ \
B C
\ /
D
*/
contract A {
// This is called an event. You can emit events from your function
// and they are logged into the transaction log.
// In our case, this will be useful for tracing function calls.
event Log(string message);
function foo() public virtual {
emit Log("A.foo called");
}
function bar() public virtual {
emit Log("A.bar called");
}
}
contract B is A {
function foo() public virtual override {
emit Log("B.foo called");
A.foo();
}
function bar() public virtual override {
emit Log("B.bar called");
super.bar();
}
}
contract C is A {
function foo() public virtual override {
emit Log("C.foo called");
A.foo();
}
function bar() public virtual override {
emit Log("C.bar called");
super.bar();
}
}
contract D is B, C {
// Try:
// - Call D.foo and check the transaction logs.
// Although D inherits A, B and C, it only called C and then A.
// - Call D.bar and check the transaction logs
// D called C, then B, and finally A.
// Although super was called twice (by B and C) it only called A once.
function foo() public override(B, C) {
super.foo();
}
function bar() public override(B, C) {
super.bar();
}
}
其中:
- 调用foo的时候,由于B,C中的foo都没有使用super,所以只是覆盖问题,根据最远继承,C
覆盖了B,所以执行顺序为:D -> C -> A - 调用bar的时候,由于B,C中的bar使用了super,此时D的两个parent都需要执行一遍,因此为D-> C -> B -> A
Try on Remix
继承的状态变量覆盖(shadowing)
状态变量无法进行重写,只能继承父类状态变量
但是可以通过在子合约的构造函数中进行覆盖,从而达到重写目的
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.13;
contract A {
string public name = "Contract A";
function getName() public view returns (string memory) {
return name;
}
}
// Shadowing is disallowed in Solidity 0.6
// This will not compile
// contract B is A {
// string public name = "Contract B";
// }
contract C is A {
// This is the correct way to override inherited state variables.
constructor() {
name = "Contract C";
}
// C.getName returns "Contract C"
}
Try on Remix
构造函数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.
// 1. Pass the parameters here in the inheritance list.
contract B is X("Input to X"), Y("Input to Y") {
}
contract C is X, Y {
// 2. 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") {}
}
Try on Remix
可见性visibility
合约的方法和状态变量需要使用关键字进行修饰,从而决定其是否可以被其他合约调用,修饰符包括:
- public:所有的合约和外部账户(EOA)都可以调用;
- private:只允许合约内部调用;
- internal:仅允许合约内部以及子合约中调用;
- external:仅允许外部合约调用,合约及子合约都不能调用;(早期版本可以使用this调用external方法)
另,状态变量可以被修饰为:publi, private, internal,但是无法修饰为external
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.13;
contract Base {
// Private function can only be called
// - inside this contract
// Contracts that inherit this contract cannot call this function.
function privateFunc() private pure returns (string memory) {
return "private function called";
}
function testPrivateFunc() public pure returns (string memory) {
return privateFunc();
}
// Internal function can be called
// - inside this contract
// - inside contracts that inherit this contract
function internalFunc() internal pure returns (string memory) {
return "internal function called";
}
function testInternalFunc() public pure virtual returns (string memory) {
return internalFunc();
}
// Public functions can be called
// - inside this contract
// - inside contracts that inherit this contract
// - by other contracts and accounts
function publicFunc() public pure returns (string memory) {
return "public function called";
}
// External functions can only be called
// - by other contracts and accounts
function externalFunc() external pure returns (string memory) {
return "external function called";
}
// This function will not compile since we're trying to call
// an external function here.
// function testExternalFunc() public pure returns (string memory) {
// return externalFunc();
// }
// State variables
string private privateVar = "my private variable";
string internal internalVar = "my internal variable";
string public publicVar = "my public variable";
// State variables cannot be external so this code won't compile.
// string external externalVar = "my external variable";
}
contract Child is Base {
// Inherited contracts do not have access to private functions
// and state variables.
// function testPrivateFunc() public pure returns (string memory) {
// return privateFunc();
// }
// Internal function call be called inside child contracts.
function testInternalFunc() public pure override returns (string memory) {
return internalFunc();
}
}
Try on Remix
abstract
手册:https://docs.soliditylang.org/en/v0.8.14/contracts.html?highlight=abstract#abstract-contracts
抽象合约的作用是将函数定义和具体实现分离,从而实现解耦、可拓展性,其使用规则为:
- 当合约中有未实现的函数时,则合约必须修饰为abstract;
- 当合约继承的base合约中有构造函数,但是当前合约并没有对其进行传参时,则必须修饰为abstract;
- abstract合约中未实现的函数必须在子合约中实现,即所有在abstract中定义的函数都必须有实现;
- abstract合约不能单独部署,必须被继承后才能部署;
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.6.0 <0.9.0;
abstract contract Animal {
string public species;
constructor(string memory _base) {
species = _base;
}
}
abstract contract Feline {
uint public num;
function utterance() public pure virtual returns (bytes32);
function base(uint _num) public returns(uint, string memory) {
num = _num;
return (num, "hello world!");
}
}
// 由于Animal中的构造函数没有进行初始化,所以必须修饰为abstract
abstract contract Cat1 is Feline, Animal {
function utterance() public pure override returns (bytes32) { return "miaow"; }
}
contract Cat2 is Feline, Animal("Animal") {
function utterance() public pure override returns (bytes32) { return "miaow"; }
}
Interface
可以使用Interface完成多个合约之间进行交互,interface有如下特性:
- 接口中定义的function不能存在具体实现;
- 接口可以继承;
- 所有的function必须定义为external;
- 接口中不能存在constructor函数;
- 接口中不能定义状态变量。
- abstract和interface的区别
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.13;
contract Counter {
uint public count;
function increment() external {
count += 1;
}
}
interface IBase {
function count() external view returns (uint);
}
interface ICounter is IBase {
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 demo:
// 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);
}
}
Try on Remix
library
库与合约类似,限制:不能在库中定义状态变量,不能向库地址中转入ether;
库有两种存在形式:
- 内嵌(embedded):当库中所有的方法都是internal时,此时会将库代码内嵌在调用合约中,不会单独部署库合约;
- 链接(linked):当库中含有external或public方法时,此时会单独将库合约部署,并在调用合约部署时链接link到库合约。
- 可以复用的代码可以编写到库中,不同的调用者可以linked到相同的库,因此会更加节约gas;
- 对于linked库合约,调用合约使用delegatecall进行调用,所以上下文为调用合约;
- 部署工具(如remix)会帮我们自动部署&链接合约库。
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.13;
// 1. 只有internal方法,会内嵌到调用合约中
library SafeMath {
function add(uint x, uint y) internal pure returns (uint) {
uint z = x + y;
require(z >= x, "uint overflow");
return z;
}
}
library Math {
function sqrt(uint y) internal pure returns (uint z) {
if (y > 3) {
z = y;
uint x = y / 2 + 1;
while (x < z) {
z = x;
x = (y / x + x) / 2;
}
} else if (y != 0) {
z = 1;
}
// else z = 0 (default value)
}
}
contract TestSafeMath {
// 对uint类型增加SafeMath的方法
using SafeMath for uint;
uint public MAX_UINT = 2**256 - 1;
// 用法1:x.方法(y)
function testAdd(uint x, uint y) public pure returns (uint) {
return x.add(y);
}
// 用法2:库.方法(x)
function testSquareRoot(uint x) public pure returns (uint) {
return Math.sqrt(x);
}
}
// 2. 存在public方法时,会单独部署库合约,并且第一个参数是状态变量类型
library Array {
// 修改调用者状态变量的方式,第一个参数是状态变量本身
function remove(uint[] storage arr, uint index) public {
// Move the last element into the place to delete
require(arr.length > 0, "Can't remove from empty array");
arr[index] = arr[arr.length - 1];
arr.pop();
}
}
contract TestArray {
using Array for uint[];
uint[] public arr;
function testArrayRemove() public {
for (uint i = 0; i < 3; i++) {
arr.push(i);
}
arr.remove(1);
assert(arr.length == 2);
assert(arr[0] == 0);
assert(arr[1] == 2);
}
}
Try on Remix
传递结构体
如何正确传值:
案例一:
https://ethereum.stackexchange.com/questions/72435/how-to-pass-struct-params-in-remix-ide
pragma solidity ^0.7;
pragma experimental ABIEncoderV2;
contract TestStruct {
struct User {
string name;
uint256 age;
}
mapping (bytes32 => User) users;
function addUsers (User [] memory _users) public {
for (uint i = 0; i < _users.length; i++) {
bytes32 hash = keccak256(abi.encode(_users[i].name));
users[hash] = _users[i];
}
}
function getUser (string memory username) public view returns (User memory) {
bytes32 hash = keccak256(abi.encode(username));
return users[hash];
}
}
输入参数:
[["Duke", 20], ["Linda", 21]]
测试成功!
案例二:
// SPDX-License-Identifier: MIT
pragma solidity =0.8.10;
pragma abicoder v2;
contract StructTest {
event Info(uint8 _type, address _addr);
struct Token {
uint8 tokenType;
address tokenAddress;
}
function call(Token [] memory tokens) external {
for (uint8 i = 0; i< tokens.length; i++) {
(uint8 t, address addr) = (tokens[i].tokenType, tokens[i].tokenAddress);
emit Info(t, addr);
}
}
}
输入参数:
[[0,0x5B38Da6a701c568545dCfcB03FcB875f56beddC4],[1,0xAb8483F64d9C6d1EcF9b849Ae677dD3315835cb2]]
但是一直失败!
原因:地址需要使用双引号包裹传递,正确参数:
[[0,"0x5B38Da6a701c568545dCfcB03FcB875f56beddC4"],[1,"0xAb8483F64d9C6d1EcF9b849Ae677dD3315835cb2"]]
当结构体中有数组
正确做法:
- 先使用storage创建空的数组对象
- 然后拼装对象,push到数组中
错误做法:
- 先new一个数组,拼装数据
- 然后赋值到结构体中

