diff --git a/01_HelloWeb3/readme.md b/01_HelloWeb3/readme.md index 48e9a547b..ad525ffbc 100644 --- a/01_HelloWeb3/readme.md +++ b/01_HelloWeb3/readme.md @@ -8,7 +8,7 @@ tags: # WTF Solidity极简入门: 1. Hello Web3 (三行代码) -我最近在重新学solidity,巩固一下细节,也写一个“WTF Solidity极简入门”,供小白们使用(编程大佬可以另找教程),每周更新1-3讲。 +我最近在重新学Solidity,巩固一下细节,也写一个“WTF Solidity极简入门”,供小白们使用(编程大佬可以另找教程),每周更新1-3讲。 推特:[@0xAA_Science](https://twitter.com/0xAA_Science) diff --git a/02_ValueTypes/readme.md b/02_ValueTypes/readme.md index 50d023fc7..d145c9d46 100644 --- a/02_ValueTypes/readme.md +++ b/02_ValueTypes/readme.md @@ -8,7 +8,7 @@ tags: # WTF Solidity极简入门: 2. 值类型 -我最近在重新学solidity,巩固一下细节,也写一个“WTF Solidity极简入门”,供小白们使用(编程大佬可以另找教程),每周更新1-3讲。 +我最近在重新学Solidity,巩固一下细节,也写一个“WTF Solidity极简入门”,供小白们使用(编程大佬可以另找教程),每周更新1-3讲。 推特:[@0xAA_Science](https://twitter.com/0xAA_Science) @@ -19,6 +19,7 @@ tags: ----- ## Solidity中的变量类型 + 1. **值类型(Value Type)**:包括布尔型,整数型等等,这类变量赋值时候直接传递数值。 2. **引用类型(Reference Type)**:包括数组和结构体,这类变量占空间大,赋值时候直接传递地址(类似指针)。 @@ -28,12 +29,14 @@ tags: 我们将仅介绍常用类型,不常用的类型不会涉及,本篇将介绍值类型。 ## 值类型 + ### 1. 布尔型 + 布尔型是二值变量,取值为 `true` 或 `false`。 ```solidity - // 布尔值 - bool public _bool = true; +// 布尔值 +bool public _bool = true; ``` 布尔值的运算符包括: @@ -45,44 +48,46 @@ tags: - `!=` (不等于) ```solidity - // 布尔运算 - bool public _bool1 = !_bool; // 取非 - bool public _bool2 = _bool && _bool1; // 与 - bool public _bool3 = _bool || _bool1; // 或 - bool public _bool4 = _bool == _bool1; // 相等 - bool public _bool5 = _bool != _bool1; // 不相等 +// 布尔运算 +bool public _bool1 = !_bool; // 取非 +bool public _bool2 = _bool && _bool1; // 与 +bool public _bool3 = _bool || _bool1; // 或 +bool public _bool4 = _bool == _bool1; // 相等 +bool public _bool5 = _bool != _bool1; // 不相等 ``` 在上述代码中:变量 `_bool` 的取值是 `true`;`_bool1` 是 `_bool` 的非,为 `false`;`_bool && _bool1` 为 `false`;`_bool || _bool1` 为 `true`;`_bool == _bool1` 为 `false`;`_bool != _bool1` 为 `true`。 -**值得注意的是:**`&&` 和 `||` 运算符遵循短路规则,这意味着,假如存在 `f(x) || g(y)` 的表达式,如果 `f(x)` 是 `true`,`g(y)` 不会被计算,即使它和 `f(x)` 的结果是相反的。 +**值得注意的是:**`&&` 和 `||` 运算符遵循短路规则,这意味着,假如存在 `f(x) || g(y)` 的表达式,如果 `f(x)` 是 `true`,`g(y)` 不会被计算,即使它和 `f(x)` 的结果是相反的。假如存在`f(x) && g(y)` 的表达式,如果 `f(x)` 是 `false`,`g(y)` 不会被计算。 ### 2. 整型 + 整型是 Solidity 中的整数,最常用的包括: ```solidity - // 整型 - int public _int = -1; // 整数,包括负数 - uint public _uint = 1; // 正整数 - uint256 public _number = 20220330; // 256位正整数 +// 整型 +int public _int = -1; // 整数,包括负数 +uint public _uint = 1; // 正整数 +uint256 public _number = 20220330; // 256位正整数 ``` 常用的整型运算符包括: -- 比较运算符(返回布尔值): `<=`, `<`,`==`, `!=`, `>=`, `>` +- 比较运算符(返回布尔值): `<=`, `<`,`==`, `!=`, `>=`, `>` - 算数运算符: `+`, `-`, `*`, `/`, `%`(取余),`**`(幂) ```solidity - // 整数运算 - uint256 public _number1 = _number + 1; // +,-,*,/ - uint256 public _number2 = 2**2; // 指数 - uint256 public _number3 = 7 % 2; // 取余数 - bool public _numberbool = _number2 > _number3; // 比大小 +// 整数运算 +uint256 public _number1 = _number + 1; // +,-,*,/ +uint256 public _number2 = 2**2; // 指数 +uint256 public _number3 = 7 % 2; // 取余数 +bool public _numberbool = _number2 > _number3; // 比大小 ``` 大家可以运行一下代码,看看这 4 个变量分别是多少。 ### 3. 地址类型 + 地址类型(address)有两类: - 普通地址(address): 存储一个 20 字节的值(以太坊地址的大小)。 @@ -91,11 +96,11 @@ tags: 我们会在之后的章节更加详细地介绍 payable address。 ```solidity - // 地址 - address public _address = 0x7A58c0Be72BE218B41C608b7Fe7C5bB630736C71; - address payable public _address1 = payable(_address); // payable address,可以转账、查余额 - // 地址类型的成员 - uint256 public balance = _address1.balance; // balance of address +// 地址 +address public _address = 0x7A58c0Be72BE218B41C608b7Fe7C5bB630736C71; +address payable public _address1 = payable(_address); // payable address,可以转账、查余额 +// 地址类型的成员 +uint256 public balance = _address1.balance; // balance of address ``` ### 4. 定长字节数组 @@ -106,9 +111,9 @@ tags: - 不定长字节数组: 属于引用类型(之后的章节介绍),数组长度在声明之后可以改变,包括 `bytes` 等。 ```solidity - // 固定长度的字节数组 - bytes32 public _byte32 = "MiniSolidity"; - bytes1 public _byte = _byte32[0]; +// 固定长度的字节数组 +bytes32 public _byte32 = "MiniSolidity"; +bytes1 public _byte = _byte32[0]; ``` 在上述代码中,`MiniSolidity` 变量以字节的方式存储进变量 `_byte32`。如果把它转换成 `16 进制`,就是:`0x4d696e69536f6c69646974790000000000000000000000000000000000000000` @@ -116,27 +121,35 @@ tags: `_byte` 变量的值为 `_byte32` 的第一个字节,即 `0x4d`。 ### 5. 枚举 enum + 枚举(`enum`)是 Solidity 中用户定义的数据类型。它主要用于为 `uint` 分配名称,使程序易于阅读和维护。它与 `C 语言` 中的 `enum` 类似,使用名称来代替从 `0` 开始的 `uint`: + ```solidity - // 用enum将uint 0, 1, 2表示为Buy, Hold, Sell - enum ActionSet { Buy, Hold, Sell } - // 创建enum变量 action - ActionSet action = ActionSet.Buy; +// 用enum将uint 0, 1, 2表示为Buy, Hold, Sell +enum ActionSet { Buy, Hold, Sell } +// 创建enum变量 action +ActionSet action = ActionSet.Buy; ``` + 枚举可以显式地和 `uint` 相互转换,并会检查转换的正整数是否在枚举的长度内,否则会报错: + ```solidity - // enum可以和uint显式的转换 - function enumToUint() external view returns(uint){ - return uint(action); - } +// enum可以和uint显式的转换 +function enumToUint() external view returns(uint){ + return uint(action); +} ``` + `enum` 是一个比较冷门的变量,几乎没什么人用。 ## 在 Remix 上运行 + - 部署合约后可以查看每个类型的变量的数值: + ![2-1.png](./img/2-1.png) - `enum` 和 `uint` 转换的示例: + ![2-2.png](./img/2-2.png) ![2-3.png](./img/2-3.png) diff --git a/03_Function/Function.sol b/03_Function/Function.sol index 6baa1e8ea..246c432ec 100644 --- a/03_Function/Function.sol +++ b/03_Function/Function.sol @@ -22,7 +22,7 @@ contract FunctionTypes{ new_number = number + 1; } - // internal: 内部 + // internal: 内部函数 function minus() internal { number = number - 1; } diff --git a/03_Function/readme.md b/03_Function/readme.md index 9b5a551c7..e7d27eeca 100644 --- a/03_Function/readme.md +++ b/03_Function/readme.md @@ -8,7 +8,7 @@ tags: # WTF Solidity极简入门: 3. 函数 -我最近在重新学solidity,巩固一下细节,也写一个“WTF Solidity极简入门”,供小白们使用(编程大佬可以另找教程),每周更新1-3讲。 +我最近在重新学Solidity,巩固一下细节,也写一个“WTF Solidity极简入门”,供小白们使用(编程大佬可以另找教程),每周更新1-3讲。 推特:[@0xAA_Science](https://twitter.com/0xAA_Science) @@ -25,7 +25,7 @@ Solidity语言的函数非常灵活,可以进行各种复杂操作。在本教 我们先看一下 Solidity 中函数的形式: ```solidity - function () {internal|external|public|private} [pure|view|payable] [returns ()] +function () {internal|external|public|private} [pure|view|payable] [returns ()] ``` 看着有一些复杂,让我们从前往后逐个解释(方括号中的是可写可不 @@ -53,6 +53,7 @@ Solidity语言的函数非常灵活,可以进行各种复杂操作。在本教 6. `[returns ()]`:函数返回的变量类型和名称。 ## 到底什么是 `Pure` 和`View`? + 刚开始学习 `solidity` 时,`pure` 和 `view` 关键字可能令人费解,因为其他编程语言中没有类似的关键字。`solidity` 引入这两个关键字主要是因为 以太坊交易需要支付气费(gas fee)。合约的状态变量存储在链上,gas fee 很贵,如果计算不改变链上状态,就可以不用付 `gas`。包含 `pure` 和 `view` 关键字的函数是不改写链上状态的,因此用户直接调用它们是不需要付 gas 的(注意,合约中非 `pure`/`view` 函数调用 `pure`/`view` 函数时需要付gas)。 在以太坊中,以下语句被视为修改链上状态: @@ -68,7 +69,6 @@ Solidity语言的函数非常灵活,可以进行各种复杂操作。在本教 为了帮助大家理解,我画了一个马里奥插图。在这幅插图中,我将合约中的状态变量(存储在链上)比作碧琪公主,三种不同的角色代表不同的关键字。 - ![WTF is pure and view in solidity?](https://images.mirror-media.xyz/publication-images/1B9kHsTYnDY_QURSWMmPb.png?height=1028&width=1758) - `pure`,中文意思是“纯”,这里可以理解为”纯打酱油的”。`pure` 函数既不能读取也不能写入链上的状态变量。就像小怪一样,看不到也摸不到碧琪公主。 @@ -78,39 +78,46 @@ Solidity语言的函数非常灵活,可以进行各种复杂操作。在本教 - 非 `pure` 或 `view` 的函数既可以读取也可以写入状态变量。类似马里奥里的 `boss`,可以对碧琪公主为所欲为🐶。 ## 代码 + ### 1. pure 和 view 我们在合约里定义一个状态变量 `number`,初始化为 5。 + ```solidity - // SPDX-License-Identifier: MIT - pragma solidity ^0.8.4; - contract FunctionTypes{ - uint256 public number = 5; - } +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.4; +contract FunctionTypes{ + uint256 public number = 5; +} ``` + 定义一个 `add()` 函数,每次调用会让 `number` 增加 1。 + ```solidity - // 默认 - function add() external{ - number = number + 1; - } +// 默认function +function add() external{ + number = number + 1; +} ``` + 如果 `add()` 函数被标记为 `pure`,比如 `function add() external pure`,就会报错。因为 `pure` 是不配读取合约里的状态变量的,更不配改写。那 `pure` 函数能做些什么?举个例子,你可以给函数传递一个参数 `_number`,然后让他返回 `_number + 1`,这个操作不会读取或写入状态变量。 + ```solidity - // pure: - function addPure(uint256 _number) external pure returns(uint256 new_number){ - new_number = _number + 1; - } +// pure: 纯纯牛马 +function addPure(uint256 _number) external pure returns(uint256 new_number){ + new_number = _number + 1; +} ``` ![3-3.png](./img/3-3.png) 如果 `add()` 函数被标记为 `view`,比如 `function add() external view`,也会报错。因为 `view` 能读取,但不能够改写状态变量。我们可以稍微改写下函数,读取但是不改写 `number`,返回一个新的变量。 + ```solidity - // view: 看客 - function addView() external view returns(uint256 new_number) { - new_number = number + 1; - } +// view: 看客 +function addView() external view returns(uint256 new_number) { + new_number = number + 1; +} ``` ![3-4.png](./img/3-4.png) @@ -118,37 +125,41 @@ Solidity语言的函数非常灵活,可以进行各种复杂操作。在本教 ### 2. internal v.s. external ```solidity - // internal: 内部函数 - function minus() internal { - number = number - 1; - } - - // 合约内的函数可以调用内部函数 - function minusCall() external { - minus(); - } +// internal: 内部函数 +function minus() internal { + number = number - 1; +} + +// 合约内的函数可以调用内部函数 +function minusCall() external { + minus(); +} ``` + 我们定义一个 `internal` 的 `minus()` 函数,每次调用使得 `number` 变量减少 1。由于 `internal` 函数只能由合约内部调用,我们必须再定义一个 `external` 的 `minusCall()` 函数,通过它间接调用内部的 `minus()` 函数。 + ![3-1.png](./img/3-1.png) ### 3. payable + ```solidity - // payable: 递钱,能给合约支付eth的函数 - function minusPayable() external payable returns(uint256 balance) { - minus(); - balance = address(this).balance; - } +// payable: 递钱,能给合约支付eth的函数 +function minusPayable() external payable returns(uint256 balance) { + minus(); + balance = address(this).balance; +} ``` -我们定义一个 `external payable` 的 `minusPayable()` 函数,间接的调用 `minus()`,并且返回合约里的 ETH 余额(`this` 关键字可以让我们引用合约地址)。我们可以在调用 `minusPayable()` 时往合约里转入1个 ETH。 +我们定义一个 `external payable` 的 `minusPayable()` 函数,间接的调用 `minus()`,并且返回合约里的 ETH 余额(`this` 关键字可以让我们引用合约地址)。我们可以在调用 `minusPayable()` 时往合约里转入1个 ETH。 -![](https://images.mirror-media.xyz/publication-images/ETDPN8myq7jFfAL8CUAFt.png?height=148&width=588) +![mirror-image-1](https://images.mirror-media.xyz/publication-images/ETDPN8myq7jFfAL8CUAFt.png?height=148&width=588) 我们可以在返回的信息中看到,合约的余额变为 1 ETH。 -![](https://images.mirror-media.xyz/publication-images/nGZ2pz0MvzgXuKrENJPYf.png?height=128&width=1130) +![mirror-image-2](https://images.mirror-media.xyz/publication-images/nGZ2pz0MvzgXuKrENJPYf.png?height=128&width=1130) ![3-2.png](./img/3-2.png) ## 总结 + 在这一讲,我们介绍了 `Solidity` 中的函数。`pure` 和 `view` 关键字比较难理解,在其他语言中没出现过:`view` 函数可以读取状态变量,但不能改写;`pure` 函数既不能读取也不能改写状态变量。 diff --git a/04_Return/readme.md b/04_Return/readme.md index 22f506735..10850a00a 100644 --- a/04_Return/readme.md +++ b/04_Return/readme.md @@ -9,7 +9,7 @@ tags: # WTF Solidity极简入门: 4. 函数输出 -我最近在重新学solidity,巩固一下细节,也写一个“WTF Solidity极简入门”,供小白们使用(编程大佬可以另找教程),每周更新1-3讲。 +我最近在重新学Solidity,巩固一下细节,也写一个“WTF Solidity极简入门”,供小白们使用(编程大佬可以另找教程),每周更新1-3讲。 推特:[@0xAA_Science](https://twitter.com/0xAA_Science) @@ -31,8 +31,8 @@ Solidity 中与函数输出相关的有两个关键字:`return`和`returns`。 ```solidity // 返回多个变量 function returnMultiple() public pure returns(uint256, bool, uint256[3] memory){ - return(1, true, [uint256(1),2,5]); - } + return(1, true, [uint256(1),2,5]); +} ``` 在上述代码中,我们利用 `returns` 关键字声明了有多个返回值的 `returnMultiple()` 函数,然后我们在函数主体中使用 `return(1, true, [uint256(1),2,5])` 确定了返回值。你可能会疑惑 uint256[3] memory 和 [uint256(1), 2,5] 这两个写法,你可以先在网上搜一下相关的说明或者带着这个疑惑继续学习后面的章节,你就会得到答案了。 @@ -53,41 +53,39 @@ function returnNamed() public pure returns(uint256 _number, bool _bool, uint256[ 在上述代码中,我们用 `returns(uint256 _number, bool _bool, uint256[3] memory _array)` 声明了返回变量类型以及变量名。这样,在主体中只需为变量 `_number`、`_bool`和`_array` 赋值,即可自动返回。 当然,你也可以在命名式返回中用 `return` 来返回变量: + ```solidity // 命名式返回,依然支持return function returnNamed2() public pure returns(uint256 _number, bool _bool, uint256[3] memory _array){ return(1, true, [uint256(1),2,5]); } ``` + ## 解构式赋值 -Solidity支持使用解构式赋值规则来读取函数的全部或部分返回值。 +Solidity 支持使用解构式赋值规则来读取函数的全部或部分返回值。 - 读取所有返回值:声明变量,然后将要赋值的变量用`,`隔开,按顺序排列。 -```solidity -uint256 _number; -bool _bool; -uint256[3] memory _array; -(_number, _bool, _array) = returnNamed(); -``` + ```solidity + uint256 _number; + bool _bool; + uint256[3] memory _array; + (_number, _bool, _array) = returnNamed(); + ``` - 读取部分返回值:声明要读取的返回值对应的变量,不读取的留空。在下面的代码中,我们只读取`_bool`,而不读取返回的`_number`和`_array`: -```solidity -(, _bool2, ) = returnNamed(); -``` + ```solidity + (, _bool2, ) = returnNamed(); + ``` ## 在 Remix 上运行 - 部署合约后查看三种返回方式的结果 -![](./img/4-1.png) - + ![4-1.png](./img/4-1.png) ## 总结 -这一讲,我们介绍 Solidity 函数返回值,包括:返回多种变量,命名式返回,以及利用解构式赋值读取全部或部分返回值。这些知识点有助于我们在编写智能合约时更灵活地处理函数返回值。 - - - +这一讲,我们介绍 Solidity 函数返回值,包括:返回多种变量,命名式返回,以及利用解构式赋值读取全部或部分返回值。这些知识点有助于我们在编写智能合约时,更灵活地处理函数返回值。 diff --git a/05_DataStorage/readme.md b/05_DataStorage/readme.md index 3d81bbbde..84dea721f 100644 --- a/05_DataStorage/readme.md +++ b/05_DataStorage/readme.md @@ -9,7 +9,7 @@ tags: # WTF Solidity极简入门: 5. 变量数据存储和作用域 storage/memory/calldata -我最近在重新学solidity,巩固一下细节,也写一个“WTF Solidity极简入门”,供小白们使用(编程大佬可以另找教程),每周更新1-3讲。 +我最近在重新学Solidity,巩固一下细节,也写一个“WTF Solidity极简入门”,供小白们使用(编程大佬可以另找教程),每周更新1-3讲。 推特:[@0xAA_Science](https://twitter.com/0xAA_Science) @@ -20,11 +20,12 @@ tags: ----- ## Solidity中的引用类型 + **引用类型(Reference Type)**:包括数组(`array`)和结构体(`struct`),由于这类变量比较复杂,占用存储空间大,我们在使用时必须要声明数据存储的位置。 ## 数据位置 -solidity数据存储位置有三类:`storage`,`memory`和`calldata`。不同存储位置的`gas`成本不同。`storage`类型的数据存在链上,类似计算机的硬盘,消耗`gas`多;`memory`和`calldata`类型的临时存在内存里,消耗`gas`少。大致用法: +Solidity数据存储位置有三类:`storage`,`memory`和`calldata`。不同存储位置的`gas`成本不同。`storage`类型的数据存在链上,类似计算机的硬盘,消耗`gas`多;`memory`和`calldata`类型的临时存在内存里,消耗`gas`少。大致用法: 1. `storage`:合约里的状态变量默认都是`storage`,存储在链上。 @@ -33,44 +34,50 @@ solidity数据存储位置有三类:`storage`,`memory`和`calldata`。不同 3. `calldata`:和`memory`类似,存储在内存中,不上链。与`memory`的不同点在于`calldata`变量不能修改(`immutable`),一般用于函数的参数。例子: ```solidity - function fCalldata(uint[] calldata _x) public pure returns(uint[] calldata){ - //参数为calldata数组,不能被修改 - // _x[0] = 0 //这样修改会报错 - return(_x); - } +function fCalldata(uint[] calldata _x) public pure returns(uint[] calldata){ + //参数为calldata数组,不能被修改 + // _x[0] = 0 //这样修改会报错 + return(_x); +} ``` + **Example:** + ![5-1.png](./img/5-1.png) ### 数据位置和赋值规则 + 在不同存储类型相互赋值时候,有时会产生独立的副本(修改新变量不会影响原变量),有时会产生引用(修改新变量会影响原变量)。规则如下: - 赋值本质上是创建**引用**指向本体,因此修改本体或者是引用,变化可以被同步: - - `storage`(合约的状态变量)赋值给本地`storage`(函数里的)时候,会创建引用,改变新变量会影响原变量。例子: - ```solidity - uint[] x = [1,2,3]; // 状态变量:数组 x - - function fStorage() public{ - //声明一个storage的变量 xStorage,指向x。修改xStorage也会影响x - uint[] storage xStorage = x; - xStorage[0] = 100; - } - ``` - **Example:** - ![5-2.png](./img/5-2.png) + - `storage`(合约的状态变量)赋值给本地`storage`(函数里的)时候,会创建引用,改变新变量会影响原变量。例子: + + ```solidity + uint[] x = [1,2,3]; // 状态变量:数组 x + function fStorage() public{ + //声明一个storage的变量 xStorage,指向x。修改xStorage也会影响x + uint[] storage xStorage = x; + xStorage[0] = 100; + } + ``` + **Example:** - - `memory`赋值给`memory`,会创建引用,改变新变量会影响原变量。 + ![5-2.png](./img/5-2.png) + - `memory`赋值给`memory`,会创建引用,改变新变量会影响原变量。 - - 其他情况下,赋值创建的是本体的副本,即对二者之一的修改,并不会同步到另一方 +- 其他情况下,赋值创建的是本体的副本,即对二者之一的修改,并不会同步到另一方 ## 变量的作用域 + `Solidity`中变量按作用域划分有三种,分别是状态变量(state variable),局部变量(local variable)和全局变量(global variable) + ### 1. 状态变量 -状态变量是数据存储在链上的变量,所有合约内函数都可以访问 -,`gas`消耗高。状态变量在合约内、函数外声明: + +状态变量是数据存储在链上的变量,所有合约内函数都可以访问,`gas`消耗高。状态变量在合约内、函数外声明: + ```solidity contract Variables { uint public x = 1; @@ -80,85 +87,97 @@ contract Variables { ``` 我们可以在函数里更改状态变量的值: + ```solidity - function foo() external{ - // 可以在函数里更改状态变量的值 - x = 5; - y = 2; - z = "0xAA"; - } +function foo() external{ + // 可以在函数里更改状态变量的值 + x = 5; + y = 2; + z = "0xAA"; +} ``` ### 2. 局部变量 + 局部变量是仅在函数执行过程中有效的变量,函数退出后,变量无效。局部变量的数据存储在内存里,不上链,`gas`低。局部变量在函数内声明: + ```solidity - function bar() external pure returns(uint){ - uint xx = 1; - uint yy = 3; - uint zz = xx + yy; - return(zz); - } +function bar() external pure returns(uint){ + uint xx = 1; + uint yy = 3; + uint zz = xx + yy; + return(zz); +} ``` ### 3. 全局变量 + 全局变量是全局范围工作的变量,都是`solidity`预留关键字。他们可以在函数内不声明直接使用: ```solidity - function global() external view returns(address, uint, bytes memory){ - address sender = msg.sender; - uint blockNum = block.number; - bytes memory data = msg.data; - return(sender, blockNum, data); - } +function global() external view returns(address, uint, bytes memory){ + address sender = msg.sender; + uint blockNum = block.number; + bytes memory data = msg.data; + return(sender, blockNum, data); +} ``` + 在上面例子里,我们使用了3个常用的全局变量:`msg.sender`, `block.number`和`msg.data`,他们分别代表请求发起地址,当前区块高度,和请求数据。下面是一些常用的全局变量,更完整的列表请看这个[链接](https://learnblockchain.cn/docs/solidity/units-and-global-variables.html#special-variables-and-functions): -- `blockhash(uint blockNumber)`: (`bytes32`)给定区块的哈希值 – 只适用于256最近区块, 不包含当前区块。 +- `blockhash(uint blockNumber)`: (`bytes32`) 给定区块的哈希值 – 只适用于256最近区块, 不包含当前区块。 - `block.coinbase`: (`address payable`) 当前区块矿工的地址 -- `block.gaslimit`: (`uint`) 当前区块的gaslimit -- `block.number`: (`uint`) 当前区块的number -- `block.timestamp`: (`uint`) 当前区块的时间戳,为unix纪元以来的秒 -- `gasleft()`: (`uint256`) 剩余 gas -- `msg.data`: (`bytes calldata`) 完整call data -- `msg.sender`: (`address payable`) 消息发送者 (当前 caller) -- `msg.sig`: (`bytes4`) calldata的前四个字节 (function identifier) -- `msg.value`: (`uint`) 当前交易发送的`wei`值 +- `block.gaslimit`: (`uint`) 当前区块的gaslimit +- `block.number`: (`uint`) 当前区块的number +- `block.timestamp`: (`uint`) 当前区块的时间戳,为unix纪元以来的秒 +- `gasleft()`: (`uint256`) 剩余 gas +- `msg.data`: (`bytes calldata`) 完整call data +- `msg.sender`: (`address payable`) 消息发送者 (当前 caller) +- `msg.sig`: (`bytes4`) calldata的前四个字节 (function identifier) +- `msg.value`: (`uint`) 当前交易发送的`wei`值 **Example:** + ![5-4.png](./img/5-4.png) ### 4. 全局变量-以太单位与时间单位 + #### 以太单位 + `Solidity`中不存在小数点,以`0`代替为小数点,来确保交易的精确度,并且防止精度的损失,利用以太单位可以避免误算的问题,方便程序员在合约中处理货币交易。 + - `wei`: 1 - `gwei`: 1e9 = 1000000000 - `ether`: 1e18 = 1000000000000000000 ```solidity - function weiUnit() external pure returns(uint) { - assert(1 wei == 1e0); - assert(1 wei == 1); - return 1 wei; - } +function weiUnit() external pure returns(uint) { + assert(1 wei == 1e0); + assert(1 wei == 1); + return 1 wei; +} - function gweiUnit() external pure returns(uint) { - assert(1 gwei == 1e9); - assert(1 gwei == 1000000000); - return 1 gwei; - } +function gweiUnit() external pure returns(uint) { + assert(1 gwei == 1e9); + assert(1 gwei == 1000000000); + return 1 gwei; +} - function etherUnit() external pure returns(uint) { - assert(1 ether == 1e18); - assert(1 ether == 1000000000000000000); - return 1 ether; - } +function etherUnit() external pure returns(uint) { + assert(1 ether == 1e18); + assert(1 ether == 1000000000000000000); + return 1 ether; +} ``` **Example:** + ![5-5.png](./img/5-5.png) #### 时间单位 + 可以在合约中规定一个操作必须在一周内完成,或者某个事件在一个月后发生。这样就能让合约的执行可以更加精确,不会因为技术上的误差而影响合约的结果。因此,时间单位在`Solidity`中是一个重要的概念,有助于提高合约的可读性和可维护性。 + - `seconds`: 1 - `minutes`: 60 seconds = 60 - `hours`: 60 minutes = 3600 @@ -166,40 +185,40 @@ contract Variables { - `weeks`: 7 days = 604800 ```solidity - function secondsUnit() external pure returns(uint) { - assert(1 seconds == 1); - return 1 seconds; - } +function secondsUnit() external pure returns(uint) { + assert(1 seconds == 1); + return 1 seconds; +} - function minutesUnit() external pure returns(uint) { - assert(1 minutes == 60); - assert(1 minutes == 60 seconds); - return 1 minutes; - } +function minutesUnit() external pure returns(uint) { + assert(1 minutes == 60); + assert(1 minutes == 60 seconds); + return 1 minutes; +} - function hoursUnit() external pure returns(uint) { - assert(1 hours == 3600); - assert(1 hours == 60 minutes); - return 1 hours; - } +function hoursUnit() external pure returns(uint) { + assert(1 hours == 3600); + assert(1 hours == 60 minutes); + return 1 hours; +} - function daysUnit() external pure returns(uint) { - assert(1 days == 86400); - assert(1 days == 24 hours); - return 1 days; - } +function daysUnit() external pure returns(uint) { + assert(1 days == 86400); + assert(1 days == 24 hours); + return 1 days; +} - function weeksUnit() external pure returns(uint) { - assert(1 weeks == 604800); - assert(1 weeks == 7 days); - return 1 weeks; - } +function weeksUnit() external pure returns(uint) { + assert(1 weeks == 604800); + assert(1 weeks == 7 days); + return 1 weeks; +} ``` **Example:** -![5-6.png](./img/5-6.png) +![5-6.png](./img/5-6.png) ## 总结 -在这一讲,我们介绍了`solidity`中的引用类型,数据位置和变量的作用域。重点是`storage`, `memory`和`calldata`三个关键字的用法。他们出现的原因是为了节省链上有限的存储空间和降低`gas`。下一讲我们会介绍引用类型中的数组。 +在这一讲,我们介绍了`Solidity`中的引用类型,数据位置和变量的作用域。重点是`storage`, `memory`和`calldata`三个关键字的用法。他们出现的原因是为了节省链上有限的存储空间和降低`gas`。下一讲我们会介绍引用类型中的数组。 diff --git a/06_ArrayAndStruct/ArrayAndStruct.sol b/06_ArrayAndStruct/ArrayAndStruct.sol index 0096ca6b9..ebef398cf 100644 --- a/06_ArrayAndStruct/ArrayAndStruct.sol +++ b/06_ArrayAndStruct/ArrayAndStruct.sol @@ -48,7 +48,7 @@ contract StructTypes { _student.score = 100; } - // 方法2:直接引用状态变量的struct + // 方法2:直接引用状态变量的struct function initStudent2() external{ student.id = 1; student.score = 80; diff --git a/06_ArrayAndStruct/readme.md b/06_ArrayAndStruct/readme.md index 629cfc12d..ea8d1cae6 100644 --- a/06_ArrayAndStruct/readme.md +++ b/06_ArrayAndStruct/readme.md @@ -9,130 +9,156 @@ tags: # WTF Solidity极简入门: 6. 引用类型, array, struct -我最近在重新学solidity,巩固一下细节,也写一个“WTF Solidity极简入门”,供小白们使用(编程大佬可以另找教程),每周更新1-3讲。 +我最近在重新学Solidity,巩固一下细节,也写一个“WTF Solidity极简入门”,供小白们使用(编程大佬可以另找教程),每周更新1-3讲。 -欢迎关注我的推特:[@0xAA_Science](https://twitter.com/0xAA_Science) +推特:[@0xAA_Science](https://twitter.com/0xAA_Science) -所有代码开源在github(64个star开微信交流群,已开[填表加入](https://docs.google.com/forms/d/e/1FAIpQLSe4KGT8Sh6sJ7hedQRuIYirOoZK_85miz3dw7vA1-YjodgJ-A/viewform);128个star录教学视频): [github.com/AmazingAng/WTFSolidity](https://github.com/AmazingAng/WTFSolidity) +社区:[Discord](https://discord.gg/5akcruXrsk)|[微信群](https://docs.google.com/forms/d/e/1FAIpQLSe4KGT8Sh6sJ7hedQRuIYirOoZK_85miz3dw7vA1-YjodgJ-A/viewform?usp=sf_link)|[官网 wtf.academy](https://wtf.academy) + +所有代码和教程开源在github: [github.com/AmazingAng/WTFSolidity](https://github.com/AmazingAng/WTFSolidity) ----- -这一讲,我们将介绍`solidity`中的两个重要变量类型:数组(`array`)和结构体(`struct`)。 +这一讲,我们将介绍`Solidity`中的两个重要变量类型:数组(`array`)和结构体(`struct`)。 ## 数组 array -数组(`Array`)是`solidity`常用的一种变量类型,用来存储一组数据(整数,字节,地址等等)。数组分为固定长度数组和可变长度数组两种: + +数组(`Array`)是`Solidity`常用的一种变量类型,用来存储一组数据(整数,字节,地址等等)。数组分为固定长度数组和可变长度数组两种: - 固定长度数组:在声明时指定数组的长度。用`T[k]`的格式声明,其中`T`是元素的类型,`k`是长度,例如: -```solidity + + ```solidity // 固定长度 Array uint[8] array1; bytes1[5] array2; address[100] array3; -``` + ``` + - 可变长度数组(动态数组):在声明时不指定数组的长度。用`T[]`的格式声明,其中`T`是元素的类型,例如: -```solidity + + ```solidity // 可变长度 Array uint[] array4; bytes1[] array5; address[] array6; bytes array7; -``` -**注意**:`bytes`比较特殊,是数组,但是不用加`[]`。另外,不能用`byte[]`声明单字节数组,可以使用`bytes`或`bytes1[]`。`bytes` 比 `bytes1[]` 省gas。 + ``` + + **注意**:`bytes`比较特殊,是数组,但是不用加`[]`。另外,不能用`byte[]`声明单字节数组,可以使用`bytes`或`bytes1[]`。`bytes` 比 `bytes1[]` 省gas。 ### 创建数组的规则 -在solidity里,创建数组有一些规则: + +在Solidity里,创建数组有一些规则: - 对于`memory`修饰的`动态数组`,可以用`new`操作符来创建,但是必须声明长度,并且声明后长度不能改变。例子: -```solidity + + ```solidity // memory动态数组 uint[] memory array8 = new uint[](5); bytes memory array9 = new bytes(9); -``` -- 数组字面常数(Array Literals)是写作表达式形式的数组,用方括号包着来初始化array的一种方式,并且里面每一个元素的type是以第一个元素为准的,例如`[1,2,3]`里面所有的元素都是`uint8`类型,因为在solidity中如果一个值没有指定type的话,默认就是最小单位的该type,这里 `uint` 的默认最小单位类型就是`uint8`。而`[uint(1),2,3]`里面的元素都是`uint`类型,因为第一个元素指定了是`uint`类型了,我们都以第一个元素为准。 + ``` -下面的例子中,如果没有对传入 `g()` 函数的数组进行 `uint` 转换,是会报错的。 +- 数组字面常数(Array Literals)是写作表达式形式的数组,用方括号包着来初始化array的一种方式,并且里面每一个元素的type是以第一个元素为准的,例如`[1,2,3]`里面所有的元素都是`uint8`类型,因为在Solidity中,如果一个值没有指定type的话,默认就是最小单位的该type,这里 `uint` 的默认最小单位类型就是`uint8`。而`[uint(1),2,3]`里面的元素都是`uint`类型,因为第一个元素指定了是`uint`类型了,我们都以第一个元素为准。 -```solidity -// SPDX-License-Identifier: GPL-3.0 -pragma solidity >=0.4.16 <0.9.0; + 下面的例子中,如果没有对传入 `g()` 函数的数组进行 `uint` 转换,是会报错的。 -contract C { - function f() public pure { - g([uint(1), 2, 3]); - } - function g(uint[3] memory _data) public pure { - // ... + ```solidity + // SPDX-License-Identifier: GPL-3.0 + pragma solidity >=0.4.16 <0.9.0; + + contract C { + function f() public pure { + g([uint(1), 2, 3]); + } + function g(uint[3] memory _data) public pure { + // ... + } } -} -``` + ``` + - 如果创建的是动态数组,你需要一个一个元素的赋值。 -```solidity + + ```solidity uint[] memory x = new uint[](3); x[0] = 1; x[1] = 3; x[2] = 4; -``` + ``` + ### 数组成员 + - `length`: 数组有一个包含元素数量的`length`成员,`memory`数组的长度在创建后是固定的。 - `push()`: `动态数组`拥有`push()`成员,可以在数组最后添加一个`0`元素,并返回该元素的引用。 - `push(x)`: `动态数组`拥有`push(x)`成员,可以在数组最后添加一个`x`元素。 - `pop()`: `动态数组`拥有`pop()`成员,可以移除数组最后一个元素。 **Example:** + ![6-1.png](./img/6-1.png) ## 结构体 struct + `Solidity`支持通过构造结构体的形式定义新的类型。结构体中的元素可以是原始类型,也可以是引用类型;结构体可以作为数组或映射的元素。创建结构体的方法: + ```solidity - // 结构体 - struct Student{ - uint256 id; - uint256 score; - } +// 结构体 +struct Student{ + uint256 id; + uint256 score; +} - Student student; // 初始一个student结构体 +Student student; // 初始一个student结构体 ``` 给结构体赋值的四种方法: ```solidity - // 给结构体赋值 - // 方法1:在函数中创建一个storage的struct引用 - function initStudent1() external{ - Student storage _student = student; // assign a copy of student - _student.id = 11; - _student.score = 100; - } +// 给结构体赋值 +// 方法1:在函数中创建一个storage的struct引用 +function initStudent1() external{ + Student storage _student = student; // assign a copy of student + _student.id = 11; + _student.score = 100; +} ``` + **Example:** + ![6-2.png](./img/6-2.png) ```solidity - // 方法2:直接引用状态变量的struct - function initStudent2() external{ - student.id = 1; - student.score = 80; - } +// 方法2:直接引用状态变量的struct +function initStudent2() external{ + student.id = 1; + student.score = 80; +} ``` + **Example:** + ![6-3.png](./img/6-3.png) ```solidity - // 方法3:构造函数式 - function initStudent3() external { - student = Student(3, 90); - } +// 方法3:构造函数式 +function initStudent3() external { + student = Student(3, 90); +} ``` + **Example:** + ![6-4.png](./img/6-4.png) ```solidity - // 方法4:key value - function initStudent4() external { - student = Student({id: 4, score: 60}); - } +// 方法4:key value +function initStudent4() external { + student = Student({id: 4, score: 60}); +} ``` + **Example:** + ![6-5.png](./img/6-5.png) + ## 总结 -这一讲,我们介绍了solidity中数组(`array`)和结构体(`struct`)的基本用法。下一讲我们将介绍solidity中的哈希表——映射(`mapping`)。 +这一讲,我们介绍了Solidity中数组(`array`)和结构体(`struct`)的基本用法。下一讲我们将介绍Solidity中的哈希表——映射(`mapping`)。 diff --git a/07_Mapping/readme.md b/07_Mapping/readme.md index 3e276c828..1a4abc0fb 100644 --- a/07_Mapping/readme.md +++ b/07_Mapping/readme.md @@ -9,7 +9,7 @@ tags: # WTF Solidity极简入门: 7. 映射类型 mapping -我最近在重新学solidity,巩固一下细节,也写一个“WTF Solidity极简入门”,供小白们使用(编程大佬可以另找教程),每周更新1-3讲。 +我最近在重新学Solidity,巩固一下细节,也写一个“WTF Solidity极简入门”,供小白们使用(编程大佬可以另找教程),每周更新1-3讲。 推特:[@0xAA_Science](https://twitter.com/0xAA_Science) @@ -22,34 +22,43 @@ tags: 这一讲,我们将介绍映射(`Mapping`)类型,Solidity中存储键值对的数据结构,可以理解为哈希表。 ## 映射Mapping + 在映射中,人们可以通过键(`Key`)来查询对应的值(`Value`),比如:通过一个人的`id`来查询他的钱包地址。 声明映射的格式为`mapping(_KeyType => _ValueType)`,其中`_KeyType`和`_ValueType`分别是`Key`和`Value`的变量类型。例子: + ```solidity mapping(uint => address) public idToAddress; // id映射到地址 mapping(address => address) public swapPair; // 币对的映射,地址到地址 -``` +``` + ## 映射的规则 + - **规则1**:映射的`_KeyType`只能选择Solidity内置的值类型,比如`uint`,`address`等,不能用自定义的结构体。而`_ValueType`可以使用自定义的类型。下面这个例子会报错,因为`_KeyType`使用了我们自定义的结构体: -```solidity -// 我们定义一个结构体 Struct -struct Student{ - uint256 id; - uint256 score; -} + + ```solidity + // 我们定义一个结构体 Struct + struct Student{ + uint256 id; + uint256 score; + } mapping(Student => uint) public testVar; -``` -- **规则2**:映射的存储位置必须是`storage`,因此可以用于合约的状态变量,函数中的`storage`变量,和library函数的参数(见[例子](https://github.com/ethereum/solidity/issues/4635))。不能用于`public`函数的参数或返回结果中,因为`mapping`记录的是一种关系 (key - value pair)。 + ``` + +- **规则2**:映射的存储位置必须是`storage`,因此可以用于合约的状态变量,函数中的`storage`变量和library函数的参数(见[例子](https://github.com/ethereum/solidity/issues/4635))。不能用于`public`函数的参数或返回结果中,因为`mapping`记录的是一种关系 (key - value pair)。 - **规则3**:如果映射声明为`public`,那么Solidity会自动给你创建一个`getter`函数,可以通过`Key`来查询对应的`Value`。 - **规则4**:给映射新增的键值对的语法为`_Var[_Key] = _Value`,其中`_Var`是映射变量名,`_Key`和`_Value`对应新增的键值对。例子: -```solidity + + ```solidity function writeMap (uint _Key, address _Value) public{ idToAddress[_Key] = _Value; } -``` + ``` + ## 映射的原理 + - **原理1**: 映射不储存任何键(`Key`)的资讯,也没有length的资讯。 - **原理2**: 映射使用`keccak256(abi.encodePacked(key, slot))`当成offset存取value,其中`slot`是映射变量定义所在的插槽位置。 @@ -57,6 +66,7 @@ struct Student{ - **原理3**: 因为Ethereum会定义所有未使用的空间为0,所以未赋值(`Value`)的键(`Key`)初始值都是各个type的默认值,如uint的默认值是0。 ## 在Remix上验证 (以 `Mapping.sol`为例) + - 映射示例 1 部署 ![7-1](./img/7-1.jpg) @@ -69,8 +79,6 @@ struct Student{ ![7-3](./img/7-3.jpg) - - ## 总结 -这一讲,我们介绍了Solidity中哈希表——映射(`Mapping`)的用法。至此,我们已经学习了所有常用变量种类,之后我们会学习控制流`if-else`,` while`等。 +这一讲,我们介绍了Solidity中哈希表——映射(`Mapping`)的用法。至此,我们已经学习了所有常用变量种类,之后我们会学习控制流`if-else`,`while`等。 diff --git a/08_InitialValue/readme.md b/08_InitialValue/readme.md index 13e3b7da0..3b6fa538a 100644 --- a/08_InitialValue/readme.md +++ b/08_InitialValue/readme.md @@ -8,7 +8,7 @@ tags: # WTF Solidity极简入门: 8. 变量初始值 -我最近在重新学solidity,巩固一下细节,也写一个“WTF Solidity极简入门”,供小白们使用(编程大佬可以另找教程),每周更新1-3讲。 +我最近在重新学Solidity,巩固一下细节,也写一个“WTF Solidity极简入门”,供小白们使用(编程大佬可以另找教程),每周更新1-3讲。 推特:[@0xAA_Science](https://twitter.com/0xAA_Science) @@ -20,7 +20,7 @@ tags: ## 变量初始值 -在`solidity`中,声明但没赋值的变量都有它的初始值或默认值。这一讲,我们将介绍常用变量的初始值。 +在`Solidity`中,声明但没赋值的变量都有它的初始值或默认值。这一讲,我们将介绍常用变量的初始值。 ### 值类型初始值 @@ -31,63 +31,70 @@ tags: - `enum`: 枚举中的第一个元素 - `address`: `0x0000000000000000000000000000000000000000` (或 `address(0)`) - `function` - - `internal`: 空白函数 - - `external`: 空白函数 + - `internal`: 空白函数 + - `external`: 空白函数 可以用`public`变量的`getter`函数验证上面写的初始值是否正确: + ```solidity - bool public _bool; // false - string public _string; // "" - int public _int; // 0 - uint public _uint; // 0 - address public _address; // 0x0000000000000000000000000000000000000000 +bool public _bool; // false +string public _string; // "" +int public _int; // 0 +uint public _uint; // 0 +address public _address; // 0x0000000000000000000000000000000000000000 - enum ActionSet { Buy, Hold, Sell} - ActionSet public _enum; // 第1个内容Buy的索引0 +enum ActionSet { Buy, Hold, Sell} +ActionSet public _enum; // 第1个内容Buy的索引0 - function fi() internal{} // internal空白函数 - function fe() external{} // external空白函数 +function fi() internal{} // internal空白函数 +function fe() external{} // external空白函数 ``` ### 引用类型初始值 -- 映射`mapping`: 所有元素都为其默认值的`mapping` +- 映射`mapping`: 所有元素都为其默认值的`mapping` - 结构体`struct`: 所有成员设为其默认值的结构体 - - 数组`array` - - 动态数组: `[]` - - 静态数组(定长): 所有成员设为其默认值的静态数组 + - 动态数组: `[]` + - 静态数组(定长): 所有成员设为其默认值的静态数组 可以用`public`变量的`getter`函数验证上面写的初始值是否正确: + ```solidity - // Reference Types - uint[8] public _staticArray; // 所有成员设为其默认值的静态数组[0,0,0,0,0,0,0,0] - uint[] public _dynamicArray; // `[]` - mapping(uint => address) public _mapping; // 所有元素都为其默认值的mapping - // 所有成员设为其默认值的结构体 0, 0 - struct Student{ - uint256 id; - uint256 score; - } - Student public student; +// Reference Types +uint[8] public _staticArray; // 所有成员设为其默认值的静态数组[0,0,0,0,0,0,0,0] +uint[] public _dynamicArray; // `[]` +mapping(uint => address) public _mapping; // 所有元素都为其默认值的mapping +// 所有成员设为其默认值的结构体 0, 0 +struct Student{ + uint256 id; + uint256 score; +} +Student public student; ``` ### `delete`操作符 + `delete a`会让变量`a`的值变为初始值。 + ```solidity - // delete操作符 - bool public _bool2 = true; - function d() external { - delete _bool2; // delete 会让_bool2变为默认值,false - } +// delete操作符 +bool public _bool2 = true; +function d() external { + delete _bool2; // delete 会让_bool2变为默认值,false +} ``` + ## 在remix上验证 + - 部署合约查看值类型、引用类型的初始值 -![](./img/8-1.png) -- 值类型、引用类型delete操作后的默认值 -![](./img/8-2.png) + ![8-1.png](./img/8-1.png) + +- 值类型、引用类型`delete`操作后的默认值 + + ![8-2.png](./img/8-2.png) ## 总结 -这一讲,我们介绍了`solidity`中变量的初始值。变量被声明但没有赋值的时候,它的值默认为初始值。不同类型的变量初始值不同,`delete`操作符可以删除一个变量的值并代替为初始值。 +这一讲,我们介绍了`Solidity`中变量的初始值。变量被声明但没有赋值的时候,它的值默认为初始值。不同类型的变量初始值不同,`delete`操作符可以删除一个变量的值并代替为初始值。 diff --git a/09_Constant/readme.md b/09_Constant/readme.md index a78064e09..ba2ad5922 100644 --- a/09_Constant/readme.md +++ b/09_Constant/readme.md @@ -9,7 +9,7 @@ tags: # WTF Solidity极简入门: 9. 常数 constant和immutable -我最近在重新学solidity,巩固一下细节,也写一个“WTF Solidity极简入门”,供小白们使用(编程大佬可以另找教程),每周更新1-3讲。 +我最近在重新学Solidity,巩固一下细节,也写一个“WTF Solidity极简入门”,供小白们使用(编程大佬可以另找教程),每周更新1-3讲。 推特:[@0xAA_Science](https://twitter.com/0xAA_Science) @@ -18,58 +18,66 @@ tags: 所有代码和教程开源在github: [github.com/AmazingAng/WTFSolidity](https://github.com/AmazingAng/WTFSolidity) ----- -这一讲,我们介绍Solidity中和常量相关的两个关键字,`constant`(常量)和`immutable`(不变量)。状态变量声明这个两个关键字之后,不能在合约后更改数值。这样做的好处是提升合约的安全性并节省`gas`。 +这一讲,我们介绍Solidity中和常量相关的两个关键字,`constant`(常量)和`immutable`(不变量)。状态变量声明这两个关键字之后,不能在初始化后更改数值。这样做的好处是提升合约的安全性并节省`gas`。 另外,只有数值变量可以声明`constant`和`immutable`;`string`和`bytes`可以声明为`constant`,但不能为`immutable`。 ## constant和immutable + ### constant + `constant`变量必须在声明的时候初始化,之后再也不能改变。尝试改变的话,编译不通过。 + ``` solidity - // constant变量必须在声明的时候初始化,之后不能改变 - uint256 constant CONSTANT_NUM = 10; - string constant CONSTANT_STRING = "0xAA"; - bytes constant CONSTANT_BYTES = "WTF"; - address constant CONSTANT_ADDRESS = 0x0000000000000000000000000000000000000000; +// constant变量必须在声明的时候初始化,之后不能改变 +uint256 constant CONSTANT_NUM = 10; +string constant CONSTANT_STRING = "0xAA"; +bytes constant CONSTANT_BYTES = "WTF"; +address constant CONSTANT_ADDRESS = 0x0000000000000000000000000000000000000000; ``` + ### immutable + `immutable`变量可以在声明时或构造函数中初始化,因此更加灵活。 + ``` solidity - // immutable变量可以在constructor里初始化,之后不能改变 - uint256 public immutable IMMUTABLE_NUM = 9999999999; - address public immutable IMMUTABLE_ADDRESS; - uint256 public immutable IMMUTABLE_BLOCK; - uint256 public immutable IMMUTABLE_TEST; +// immutable变量可以在constructor里初始化,之后不能改变 +uint256 public immutable IMMUTABLE_NUM = 9999999999; +address public immutable IMMUTABLE_ADDRESS; +uint256 public immutable IMMUTABLE_BLOCK; +uint256 public immutable IMMUTABLE_TEST; ``` -你可以使用全局变量例如`address(this)`,`block.number` ,或者自定义的函数给`immutable`变量初始化。在下面这个例子,我们利用了`test()`函数给`IMMUTABLE_TEST`初始化为`9`: + +你可以使用全局变量例如`address(this)`,`block.number` 或者自定义的函数给`immutable`变量初始化。在下面这个例子,我们利用了`test()`函数给`IMMUTABLE_TEST`初始化为`9`: + ``` solidity - // 利用constructor初始化immutable变量,因此可以利用 - constructor(){ - IMMUTABLE_ADDRESS = address(this); - IMMUTABLE_BLOCK = block.number; - IMMUTABLE_TEST = test(); - } - - function test() public pure returns(uint256){ - uint256 what = 9; - return(what); - } +// 利用constructor初始化immutable变量,因此可以利用 +constructor(){ + IMMUTABLE_ADDRESS = address(this); + IMMUTABLE_BLOCK = block.number; + IMMUTABLE_TEST = test(); +} + +function test() public pure returns(uint256){ + uint256 what = 9; + return(what); +} ``` ## 在remix上验证 + 1. 部署好合约之后,通过remix上的`getter`函数,能获取到`constant`和`immutable`变量初始化好的值。 - ![9-1.png](./img/9-1.png) - + ![9-1.png](./img/9-1.png) + 2. `constant`变量初始化之后,尝试改变它的值,会编译不通过并抛出`TypeError: Cannot assign to a constant variable.`的错误。 + ![9-2.png](./img/9-2.png) - ![9-2.png](./img/9-2.png) - 3. `immutable`变量初始化之后,尝试改变它的值,会编译不通过并抛出`TypeError: Immutable state variable already initialized.`的错误。 ![9-3.png](./img/9-3.png) ## 总结 -这一讲,我们介绍了Solidity中两个关键字,`constant`(常量)和`immutable`(不变量),让不应该变的变量保持不变。这样的做法能在节省`gas`的同时提升合约的安全性。 +这一讲,我们介绍了Solidity中两个关键字,`constant`(常量)和`immutable`(不变量),让不应该变的变量保持不变。这样的做法能在节省`gas`的同时提升合约的安全性。 diff --git a/10_InsertionSort/InsertionSort.sol b/10_InsertionSort/InsertionSort.sol index 6200e68f5..632f3f311 100644 --- a/10_InsertionSort/InsertionSort.sol +++ b/10_InsertionSort/InsertionSort.sol @@ -50,7 +50,6 @@ contract InsertionSort { // 插入排序 错误版 function insertionSortWrong(uint[] memory a) public pure returns(uint[] memory) { - // note that uint can not take negative value for (uint i = 1;i < a.length;i++){ uint temp = a[i]; uint j=i-1; diff --git a/10_InsertionSort/readme.md b/10_InsertionSort/readme.md index a15b9dad8..fab248f85 100644 --- a/10_InsertionSort/readme.md +++ b/10_InsertionSort/readme.md @@ -7,9 +7,9 @@ tags: - if-else/for/while/ternary --- -# WTF Solidity极简入门: 10. 控制流,用solidity实现插入排序 +# WTF Solidity极简入门: 10. 控制流,用Solidity实现插入排序 -我最近在重新学solidity,巩固一下细节,也写一个“WTF Solidity极简入门”,供小白们使用(编程大佬可以另找教程),每周更新1-3讲。 +我最近在重新学Solidity,巩固一下细节,也写一个“WTF Solidity极简入门”,供小白们使用(编程大佬可以另找教程),每周更新1-3讲。 推特:[@0xAA_Science](https://twitter.com/0xAA_Science) @@ -18,143 +18,159 @@ tags: 所有代码和教程开源在github: [github.com/AmazingAng/WTFSolidity](https://github.com/AmazingAng/WTFSolidity) ----- -这一讲,我们将介绍`solidity`中的控制流,然后讲如何用`solidity`实现插入排序(`InsertionSort`),一个看起来简单,但实际上很容易写出`bug`的程序。 +这一讲,我们将介绍`Solidity`中的控制流,然后讲如何用`Solidity`实现插入排序(`InsertionSort`),一个看起来简单,但实际上很容易写出`bug`的程序。 ## 控制流 + `Solidity`的控制流与其他语言类似,主要包含以下几种: 1. `if-else` -```solidity -function ifElseTest(uint256 _number) public pure returns(bool){ - if(_number == 0){ - return(true); - }else{ - return(false); + ```solidity + function ifElseTest(uint256 _number) public pure returns(bool){ + if(_number == 0){ + return(true); + }else{ + return(false); + } } -} -``` + ``` + 2. `for循环` -```solidity -function forLoopTest() public pure returns(uint256){ - uint sum = 0; - for(uint i = 0; i < 10; i++){ - sum += i; + ```solidity + function forLoopTest() public pure returns(uint256){ + uint sum = 0; + for(uint i = 0; i < 10; i++){ + sum += i; + } + return(sum); } - return(sum); -} -``` + ``` + 3. `while循环` -```solidity -function whileTest() public pure returns(uint256){ - uint sum = 0; - uint i = 0; - while(i < 10){ - sum += i; - i++; + ```solidity + function whileTest() public pure returns(uint256){ + uint sum = 0; + uint i = 0; + while(i < 10){ + sum += i; + i++; + } + return(sum); } - return(sum); -} -``` + ``` + 4. `do-while循环` -```solidity -function doWhileTest() public pure returns(uint256){ - uint sum = 0; - uint i = 0; - do{ - sum += i; - i++; - }while(i < 10); - return(sum); -} -``` + ```solidity + function doWhileTest() public pure returns(uint256){ + uint sum = 0; + uint i = 0; + do{ + sum += i; + i++; + }while(i < 10); + return(sum); + } + ``` 5. `三元运算符` -三元运算符是`solidity`中唯一一个接受三个操作数的运算符,规则`条件? 条件为真的表达式:条件为假的表达式`。 此运算符经常用作 if 语句的快捷方式。 -```solidity -// 三元运算符 ternary/conditional operator -function ternaryTest(uint256 x, uint256 y) public pure returns(uint256){ - // return the max of x and y - return x >= y ? x: y; -} -``` + 三元运算符是`Solidity`中唯一一个接受三个操作数的运算符,规则`条件? 条件为真的表达式:条件为假的表达式`。此运算符经常用作`if`语句的快捷方式。 + + ```solidity + // 三元运算符 ternary/conditional operator + function ternaryTest(uint256 x, uint256 y) public pure returns(uint256){ + // return the max of x and y + return x >= y ? x: y; + } + ``` 另外还有`continue`(立即进入下一个循环)和`break`(跳出当前循环)关键字可以使用。 -## 用`solidity`实现插入排序 -### 写在前面:90%以上的人用`solidity`写插入算法都会出错。 +## 用`Solidity`实现插入排序 + +**写在前面:90%以上的人用`Solidity`写插入算法都会出错。** ### 插入排序 + 排序算法解决的问题是将无序的一组数字,例如`[2, 5, 3, 1]`,从小到大依次排列好。插入排序(`InsertionSort`)是最简单的一种排序算法,也是很多人学习的第一个算法。它的思路很简单,从前往后,依次将每一个数和排在他前面的数字比大小,如果比前面的数字小,就互换位置。示意图: ![插入排序](https://i.pinimg.com/originals/92/b0/34/92b034385c440e08bc8551c97df0a2e3.gif) ### `python`代码 + 我们可以先看一下插入排序的python代码: + ```python # Python program for implementation of Insertion Sort def insertionSort(arr): - for i in range(1, len(arr)): - key = arr[i] - j = i-1 - while j >=0 and key < arr[j] : - arr[j+1] = arr[j] - j -= 1 - arr[j+1] = key + for i in range(1, len(arr)): + key = arr[i] + j = i-1 + while j >=0 and key < arr[j] : + arr[j+1] = arr[j] + j -= 1 + arr[j+1] = key return arr ``` -### 改写成`solidity`后有`BUG`! -一共8行`python`代码就可以完成插入排序,非常简单。那么我们将它改写成`solidity`代码,将函数,变量,循环等等都做了相应的转换,只需要9行代码: + +### 改写成`Solidity`后有`BUG` + +一共8行`python`代码就可以完成插入排序,非常简单。那么我们将它改写成`Solidity`代码,将函数,变量,循环等等都做了相应的转换,只需要9行代码: + ``` solidity // 插入排序 错误版 - function insertionSortWrong(uint[] memory a) public pure returns(uint[] memory) { - - for (uint i = 1;i < a.length;i++){ - uint temp = a[i]; - uint j=i-1; - while( (j >= 0) && (temp < a[j])){ - a[j+1] = a[j]; - j--; - } - a[j+1] = temp; +function insertionSortWrong(uint[] memory a) public pure returns(uint[] memory) { + for (uint i = 1;i < a.length;i++){ + uint temp = a[i]; + uint j=i-1; + while( (j >= 0) && (temp < a[j])){ + a[j+1] = a[j]; + j--; } - return(a); + a[j+1] = temp; } + return(a); +} ``` + 那我们把改好的放到`remix`上去跑,输入`[2, 5, 3, 1]`。BOOM!有`bug`!改了半天,没找到`bug`在哪。我又去`google`搜”solidity insertion sort”,然后发现网上用`solidity`写的插入算法教程都是错的,比如:[Sorting in Solidity without Comparison](https://medium.com/coinmonks/sorting-in-solidity-without-comparison-4eb47e04ff0d) Remix decoded output 出现错误内容 + ![10-1](./img/10-1.jpg) -### 正确的solidity插入排序 -花了几个小时,在`Dapp-Learning`社群一个朋友的帮助下,终于找到了`bug`所在。`solidity`中最常用的变量类型是`uint`,也就是正整数,取到负值的话,会报`underflow`错误。而在插入算法中,变量`j`有可能会取到`-1`,引起报错。 +### 正确的Solidity插入排序 + +花了几个小时,在`Dapp-Learning`社群一个朋友的帮助下,终于找到了`bug`所在。`Solidity`中最常用的变量类型是`uint`,也就是正整数,取到负值的话,会报`underflow`错误。而在插入算法中,变量`j`有可能会取到`-1`,引起报错。 这里,我们需要把`j`加1,让它无法取到负值。正确代码: + ```solidity - // 插入排序 正确版 - function insertionSort(uint[] memory a) public pure returns(uint[] memory) { - // note that uint can not take negative value - for (uint i = 1;i < a.length;i++){ - uint temp = a[i]; - uint j=i; - while( (j >= 1) && (temp < a[j-1])){ - a[j] = a[j-1]; - j--; - } - a[j] = temp; +// 插入排序 正确版 +function insertionSort(uint[] memory a) public pure returns(uint[] memory) { + // note that uint can not take negative value + for (uint i = 1;i < a.length;i++){ + uint temp = a[i]; + uint j=i; + while( (j >= 1) && (temp < a[j-1])){ + a[j] = a[j-1]; + j--; } - return(a); + a[j] = temp; } + return(a); +} ``` + 运行后的结果: !["输入[2,5,3,1] 输出[1,2,3,5] "](https://images.mirror-media.xyz/publication-images/S-i6rwCMeXoi8eNJ0fRdB.png?height=300&width=554) ## 总结 -这一讲,我们介绍了`solidity`中控制流,并且用`solidity`写了插入排序。看起来很简单,但实际很难。这就是`solidity`,坑很多,每个月都有项目因为这些小`bug`损失几千万甚至上亿美元。掌握好基础,不断练习,才能写出更好的`solidity`代码。 +这一讲,我们介绍了`Solidity`中控制流,并且用`Solidity`写了插入排序。看起来很简单,但实际很难。这就是`Solidity`,坑很多,每个月都有项目因为这些小`bug`损失几千万甚至上亿美元。掌握好基础,不断练习,才能写出更好的`Solidity`代码。 diff --git a/11_Modifier/readme.md b/11_Modifier/readme.md index 7a525d6be..a44cf2040 100644 --- a/11_Modifier/readme.md +++ b/11_Modifier/readme.md @@ -10,7 +10,7 @@ tags: # WTF Solidity极简入门: 11. 构造函数和修饰器 -我最近在重新学solidity,巩固一下细节,也写一个“WTF Solidity极简入门”,供小白们使用(编程大佬可以另找教程),每周更新1-3讲。 +我最近在重新学Solidity,巩固一下细节,也写一个“WTF Solidity极简入门”,供小白们使用(编程大佬可以另找教程),每周更新1-3讲。 推特:[@0xAA_Science](https://twitter.com/0xAA_Science) @@ -20,22 +20,25 @@ tags: ----- -这一讲,我们将用合约权限控制(`Ownable`)的例子介绍`solidity`语言中构造函数(`constructor`)和独有的修饰器(`modifier`)。 +这一讲,我们将用合约权限控制(`Ownable`)的例子介绍`Solidity`语言中构造函数(`constructor`)和独有的修饰器(`modifier`)。 ## 构造函数 + 构造函数(`constructor`)是一种特殊的函数,每个合约可以定义一个,并在部署合约的时候自动运行一次。它可以用来初始化合约的一些参数,例如初始化合约的`owner`地址: + ```solidity - address owner; // 定义owner变量 +address owner; // 定义owner变量 - // 构造函数 - constructor() { - owner = msg.sender; // 在部署合约的时候,将owner设置为部署者的地址 - } +// 构造函数 +constructor() { + owner = msg.sender; // 在部署合约的时候,将owner设置为部署者的地址 +} ``` -**注意**⚠️:构造函数在不同的solidity版本中的语法并不一致,在Solidity 0.4.22之前,构造函数不使用 `constructor` 而是使用与合约名同名的函数作为构造函数而使用,由于这种旧写法容易使开发者在书写时发生疏漏(例如合约名叫 `Parents`,构造函数名写成 `parents`),使得构造函数变成普通函数,引发漏洞,所以0.4.22版本及之后,采用了全新的 `constructor` 写法。 +**注意**⚠️:构造函数在不同的Solidity版本中的语法并不一致,在Solidity 0.4.22之前,构造函数不使用 `constructor` 而是使用与合约名同名的函数作为构造函数而使用,由于这种旧写法容易使开发者在书写时发生疏漏(例如合约名叫 `Parents`,构造函数名写成 `parents`),使得构造函数变成普通函数,引发漏洞,所以0.4.22版本及之后,采用了全新的 `constructor` 写法。 构造函数的旧写法代码示例: + ```solidity pragma solidity =0.4.21; contract Parents { @@ -44,43 +47,53 @@ contract Parents { } } ``` + ## 修饰器 -修饰器(`modifier`)是`solidity`特有的语法,类似于面向对象编程中的`decorator`,声明函数拥有的特性,并减少代码冗余。它就像钢铁侠的智能盔甲,穿上它的函数会带有某些特定的行为。`modifier`的主要使用场景是运行函数前的检查,例如地址,变量,余额等。 +修饰器(`modifier`)是`Solidity`特有的语法,类似于面向对象编程中的装饰器(`decorator`),声明函数拥有的特性,并减少代码冗余。它就像钢铁侠的智能盔甲,穿上它的函数会带有某些特定的行为。`modifier`的主要使用场景是运行函数前的检查,例如地址,变量,余额等。 ![钢铁侠的modifier](https://images.mirror-media.xyz/publication-images/nVwXsOVmrYu8rqvKKPMpg.jpg?height=630&width=1200) 我们来定义一个叫做onlyOwner的modifier: + ```solidity - // 定义modifier - modifier onlyOwner { - require(msg.sender == owner); // 检查调用者是否为owner地址 - _; // 如果是的话,继续运行函数主体;否则报错并revert交易 - } +// 定义modifier +modifier onlyOwner { + require(msg.sender == owner); // 检查调用者是否为owner地址 + _; // 如果是的话,继续运行函数主体;否则报错并revert交易 +} ``` + 带有`onlyOwner`修饰符的函数只能被`owner`地址调用,比如下面这个例子: + ```solidity - function changeOwner(address _newOwner) external onlyOwner{ - owner = _newOwner; // 只有owner地址运行这个函数,并改变owner - } +function changeOwner(address _newOwner) external onlyOwner{ + owner = _newOwner; // 只有owner地址运行这个函数,并改变owner +} ``` -我们定义了一个`changeOwner`函数,运行他可以改变合约的`owner`,但是由于`onlyOwner`修饰符的存在,只有原先的`owner`可以调用,别人调用就会报错。这也是最常用的控制智能合约权限的方法。 -### OpenZeppelin的Ownable标准实现: -`OpenZeppelin`是一个维护`solidity`标准化代码库的组织,他的`Ownable`标准实现如下: +我们定义了一个`changeOwner`函数,运行它可以改变合约的`owner`,但是由于`onlyOwner`修饰符的存在,只有原先的`owner`可以调用,别人调用就会报错。这也是最常用的控制智能合约权限的方法。 + +### OpenZeppelin的Ownable标准实现 + +`OpenZeppelin`是一个维护`Solidity`标准化代码库的组织,他的`Ownable`标准实现如下: [https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/access/Ownable.sol](https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/access/Ownable.sol) ## Remix 演示示例 + 以 `Owner.sol` 为例。 + 1. 在 Remix 上编译部署代码。 2. 点击 `owner` 按钮查看当前 owner 变量。 - ![](img/11-1.jpg) + + ![11-1](img/11-1.jpg) 3. 以 owner 地址的用户身份,调用 `changeOwner` 函数,交易成功。 - ![](img/11-2.jpg) + + ![11-2](img/11-2.jpg) 4. 以非 owner 地址的用户身份,调用 `changeOwner` 函数,交易失败,因为modifier onlyOwner 的检查语句不满足。 - ![](img/11-3.jpg) + ![11-3](img/11-3.jpg) ## 总结 -这一讲,我们介绍了`solidity`中的构造函数和修饰符,并举了一个控制合约权限的`Ownable`合约。 +这一讲,我们介绍了`Solidity`中的构造函数和修饰符,并举了一个控制合约权限的`Ownable`合约。 diff --git a/12_Event/readme.md b/12_Event/readme.md index 980bf5465..d26b973fc 100644 --- a/12_Event/readme.md +++ b/12_Event/readme.md @@ -9,7 +9,7 @@ tags: # WTF Solidity极简入门: 12. 事件 -我最近在重新学solidity,巩固一下细节,也写一个“WTF Solidity极简入门”,供小白们使用(编程大佬可以另找教程),每周更新1-3讲。 +我最近在重新学Solidity,巩固一下细节,也写一个“WTF Solidity极简入门”,供小白们使用(编程大佬可以另找教程),每周更新1-3讲。 推特:[@0xAA_Science](https://twitter.com/0xAA_Science) @@ -19,56 +19,64 @@ tags: ----- -这一讲,我们用转账ERC20代币为例来介绍`solidity`中的事件(`event`)。 +这一讲,我们用转账ERC20代币为例来介绍`Solidity`中的事件(`event`)。 ## 事件 + `Solidity`中的事件(`event`)是`EVM`上日志的抽象,它具有两个特点: - 响应:应用程序([`ethers.js`](https://learnblockchain.cn/docs/ethers.js/api-contract.html#id18))可以通过`RPC`接口订阅和监听这些事件,并在前端做响应。 - 经济:事件是`EVM`上比较经济的存储数据的方式,每个大概消耗2,000 `gas`;相比之下,链上存储一个新变量至少需要20,000 `gas`。 ### 声明事件 + 事件的声明由`event`关键字开头,接着是事件名称,括号里面写好事件需要记录的变量类型和变量名。以`ERC20`代币合约的`Transfer`事件为例: + ```solidity event Transfer(address indexed from, address indexed to, uint256 value); ``` + 我们可以看到,`Transfer`事件共记录了3个变量`from`,`to`和`value`,分别对应代币的转账地址,接收地址和转账数量,其中`from`和`to`前面带有`indexed`关键字,他们会保存在以太坊虚拟机日志的`topics`中,方便之后检索。 ### 释放事件 + 我们可以在函数里释放事件。在下面的例子中,每次用`_transfer()`函数进行转账操作的时候,都会释放`Transfer`事件,并记录相应的变量。 + ```solidity - // 定义_transfer函数,执行转账逻辑 - function _transfer( - address from, - address to, - uint256 amount - ) external { +// 定义_transfer函数,执行转账逻辑 +function _transfer( + address from, + address to, + uint256 amount +) external { - _balances[from] = 10000000; // 给转账地址一些初始代币 + _balances[from] = 10000000; // 给转账地址一些初始代币 - _balances[from] -= amount; // from地址减去转账数量 - _balances[to] += amount; // to地址加上转账数量 + _balances[from] -= amount; // from地址减去转账数量 + _balances[to] += amount; // to地址加上转账数量 - // 释放事件 - emit Transfer(from, to, amount); - } + // 释放事件 + emit Transfer(from, to, amount); +} ``` ## EVM日志 `Log` 以太坊虚拟机(EVM)用日志`Log`来存储`Solidity`事件,每条日志记录都包含主题`topics`和数据`data`两部分。 -![](img/12-3.png) +![12-3](img/12-3.png) ### 主题 `topics` -日志的第一部分是主题数组,用于描述事件,长度不能超过`4`。它的第一个元素是事件的签名(哈希)。对于上面的`Transfer`事件,它的签名就是: +日志的第一部分是主题数组,用于描述事件,长度不能超过`4`。它的第一个元素是事件的签名(哈希)。对于上面的`Transfer`事件,它的事件哈希就是: + ```solidity keccak256("Transfer(address,address,uint256)") //0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef ``` -除了事件签名,主题还可以包含至多`3`个`indexed`参数,也就是`Transfer`事件中的`from`和`to`。 + +除了事件哈希,主题还可以包含至多`3`个`indexed`参数,也就是`Transfer`事件中的`from`和`to`。 `indexed`标记的参数可以理解为检索事件的索引“键”,方便之后搜索。每个 `indexed` 参数的大小为固定的256比特,如果参数太大了(比如字符串),就会自动计算哈希存储在主题中。 @@ -77,16 +85,20 @@ keccak256("Transfer(address,address,uint256)") 事件中不带 `indexed`的参数会被存储在 `data` 部分中,可以理解为事件的“值”。`data` 部分的变量不能被直接检索,但可以存储任意大小的数据。因此一般 `data` 部分可以用来存储复杂的数据结构,例如数组和字符串等等,因为这些数据超过了256比特,即使存储在事件的 `topics` 部分中,也是以哈希的方式存储。另外,`data` 部分的变量在存储上消耗的gas相比于 `topics` 更少。 ## `Remix`演示 + 以 `Event.sol` 合约为例,编译部署。 然后调用 `_transfer` 函数。 -![](img/12-1.jpg) + +![12-1](img/12-1.jpg) 点击右侧的交易查看详情,可以看到日志的具体内容。 -![](img/12-2.jpg) -### 在etherscan上查询事件 -我们尝试用`_transfer()`函数在`Rinkeby`测试网络上转账100代币,可以在`etherscan`上查询到相应的`tx`:[网址](https://rinkeby.etherscan.io/tx/0x8cf87215b23055896d93004112bbd8ab754f081b4491cb48c37592ca8f8a36c7)。 +![12-2](img/12-2.jpg) + +### 在Etherscan上查询事件 + +我们尝试用`_transfer()`函数在`Rinkeby`测试网络上转账100代币,可以在`Etherscan`上查询到相应的`tx`:[网址](https://rinkeby.etherscan.io/tx/0x8cf87215b23055896d93004112bbd8ab754f081b4491cb48c37592ca8f8a36c7)。 点击`Logs`按钮,就能看到事件明细: @@ -95,5 +107,5 @@ keccak256("Transfer(address,address,uint256)") `Topics`里面有三个元素,`[0]`是这个事件的哈希,`[1]`和`[2]`是我们定义的两个`indexed`变量的信息,即转账的转出地址和接收地址。`Data`里面是剩下的不带`indexed`的变量,也就是转账数量。 ## 总结 -这一讲,我们介绍了如何使用和查询`solidity`中的事件。很多链上分析工具包括`Nansen`和`Dune Analysis`都是基于事件工作的。 +这一讲,我们介绍了如何使用和查询`Solidity`中的事件。很多链上分析工具包括`Nansen`和`Dune Analysis`都是基于事件工作的。 diff --git a/13_Inheritance/DiamondInheritance.sol b/13_Inheritance/DiamondInheritance.sol index f09facd78..fcd919646 100644 --- a/13_Inheritance/DiamondInheritance.sol +++ b/13_Inheritance/DiamondInheritance.sol @@ -24,6 +24,7 @@ contract God { contract Adam is God { function foo() public virtual override { emit Log("Adam.foo called"); + super.foo(); } function bar() public virtual override { @@ -35,6 +36,7 @@ contract Adam is God { contract Eve is God { function foo() public virtual override { emit Log("Eve.foo called"); + super.foo(); } function bar() public virtual override { diff --git a/13_Inheritance/readme.md b/13_Inheritance/readme.md index 70c9cb079..e1f96c1c5 100644 --- a/13_Inheritance/readme.md +++ b/13_Inheritance/readme.md @@ -9,7 +9,7 @@ tags: # WTF Solidity极简入门: 13. 继承 -我最近在重新学solidity,巩固一下细节,也写一个“WTF Solidity极简入门”,供小白们使用(编程大佬可以另找教程),每周更新1-3讲。 +我最近在重新学Solidity,巩固一下细节,也写一个“WTF Solidity极简入门”,供小白们使用(编程大佬可以另找教程),每周更新1-3讲。 推特:[@0xAA_Science](https://twitter.com/0xAA_Science) @@ -18,23 +18,28 @@ tags: 所有代码和教程开源在github: [github.com/AmazingAng/WTFSolidity](https://github.com/AmazingAng/WTFSolidity) ----- -这一讲,我们介绍`solidity`中的继承(`inheritance`),包括简单继承,多重继承,以及修饰器(`modifier`)和构造函数(`constructor`)的继承。 +这一讲,我们介绍`Solidity`中的继承(`inheritance`),包括简单继承,多重继承,以及修饰器(`Modifier`)和构造函数(`Constructor`)的继承。 ## 继承 -继承是面向对象编程很重要的组成部分,可以显著减少重复代码。如果把合约看作是对象的话,`solidity`也是面向对象的编程,也支持继承。 + +继承是面向对象编程很重要的组成部分,可以显著减少重复代码。如果把合约看作是对象的话,`Solidity`也是面向对象的编程,也支持继承。 ### 规则 + - `virtual`: 父合约中的函数,如果希望子合约重写,需要加上`virtual`关键字。 - `override`:子合约重写了父合约中的函数,需要加上`override`关键字。 **注意**:用`override`修饰`public`变量,会重写与变量同名的`getter`函数,例如: + ```solidity mapping(address => uint256) public override balanceOf; ``` ### 简单继承 + 我们先写一个简单的爷爷合约`Yeye`,里面包含1个`Log`事件和3个`function`: `hip()`, `pop()`, `yeye()`,输出都是”Yeye”。 + ```solidity contract Yeye { event Log(string msg); @@ -53,7 +58,9 @@ contract Yeye { } } ``` + 我们再定义一个爸爸合约`Baba`,让他继承`Yeye`合约,语法就是`contract Baba is Yeye`,非常直观。在`Baba`合约里,我们重写一下`hip()`和`pop()`这两个函数,加上`override`关键字,并将他们的输出改为`”Baba”`;并且加一个新的函数`baba`,输出也是`”Baba”`。 + ```solidity contract Baba is Yeye{ // 继承两个function: hip()和pop(),输出改为Baba。 @@ -70,10 +77,12 @@ contract Baba is Yeye{ } } ``` + 我们部署合约,可以看到`Baba`合约里有4个函数,其中`hip()`和`pop()`的输出被成功改写成`”Baba”`,而继承来的`yeye()`的输出仍然是`”Yeye”`。 ### 多重继承 -`solidity`的合约可以继承多个合约。规则: + +`Solidity`的合约可以继承多个合约。规则: 1. 继承时要按辈分最高到最低的顺序排。比如我们写一个`Erzi`合约,继承`Yeye`合约和`Baba`合约,那么就要写成`contract Erzi is Yeye, Baba`,而不能写成`contract Erzi is Baba, Yeye`,不然就会报错。 @@ -82,6 +91,7 @@ contract Baba is Yeye{ 3. 重写在多个父合约中都重名的函数时,`override`关键字后面要加上所有父合约名字,例如`override(Yeye, Baba)`。 例子: + ```solidity contract Erzi is Yeye, Baba{ // 继承两个function: hip()和pop(),输出值为Erzi。 @@ -92,11 +102,15 @@ contract Erzi is Yeye, Baba{ function pop() public virtual override(Yeye, Baba) { emit Log("Erzi"); } +} ``` + 我们可以看到,`Erzi`合约里面重写了`hip()`和`pop()`两个函数,将输出改为`”Erzi”`,并且还分别从`Yeye`和`Baba`合约继承了`yeye()`和`baba()`两个函数。 ### 修饰器的继承 + `Solidity`中的修饰器(`Modifier`)同样可以继承,用法与函数继承类似,在相应的地方加`virtual`和`override`关键字即可。 + ```solidity contract Base1 { modifier exactDividedBy2And3(uint _a) virtual { @@ -124,14 +138,16 @@ contract Identifier is Base1 { `Identifier`合约可以直接在代码中使用父合约中的`exactDividedBy2And3`修饰器,也可以利用`override`关键字重写修饰器: ```solidity - modifier exactDividedBy2And3(uint _a) override { - _; - require(_a % 2 == 0 && _a % 3 == 0); - } +modifier exactDividedBy2And3(uint _a) override { + _; + require(_a % 2 == 0 && _a % 3 == 0); +} ``` ### 构造函数的继承 + 子合约有两种方法继承父合约的构造函数。举个简单的例子,父合约`A`里面有一个状态变量`a`,并由构造函数的参数来确定: + ```solidity // 构造函数的继承 abstract contract A { @@ -142,29 +158,36 @@ abstract contract A { } } ``` + 1. 在继承时声明父构造函数的参数,例如:`contract B is A(1)` 2. 在子合约的构造函数中声明构造函数的参数,例如: -```solidity -contract C is A { - constructor(uint _c) A(_c * _c) {} -} -``` + + ```solidity + contract C is A { + constructor(uint _c) A(_c * _c) {} + } + ``` + ### 调用父合约的函数 + 子合约有两种方式调用父合约的函数,直接调用和利用`super`关键字。 -1. 直接调用:子合约可以直接用`父合约名.函数名()`的方式来调用父合约函数,例如`Yeye.pop()`。 -```solidity +1. 直接调用:子合约可以直接用`父合约名.函数名()`的方式来调用父合约函数,例如`Yeye.pop()` + + ```solidity function callParent() public{ Yeye.pop(); } -``` -2. `super`关键字:子合约可以利用`super.函数名()`来调用最近的父合约函数。`solidity`继承关系按声明时从右到左的顺序是:`contract Erzi is Yeye, Baba`,那么`Baba`是最近的父合约,`super.pop()`将调用`Baba.pop()`而不是`Yeye.pop()`: -```solidity + ``` + +2. `super`关键字:子合约可以利用`super.函数名()`来调用最近的父合约函数。`Solidity`继承关系按声明时从右到左的顺序是:`contract Erzi is Yeye, Baba`,那么`Baba`是最近的父合约,`super.pop()`将调用`Baba.pop()`而不是`Yeye.pop()`: + + ```solidity function callParentSuper() public{ // 将调用最近的父合约函数,Baba.pop() super.pop(); } -``` + ``` ### 钻石继承 @@ -201,6 +224,7 @@ contract God { contract Adam is God { function foo() public virtual override { emit Log("Adam.foo called"); + super.foo(); } function bar() public virtual override { @@ -212,7 +236,7 @@ contract Adam is God { contract Eve is God { function foo() public virtual override { emit Log("Eve.foo called"); - God.foo(); + super.foo(); } function bar() public virtual override { @@ -235,30 +259,33 @@ contract people is Adam, Eve { 在这个例子中,调用合约`people`中的`super.bar()`会依次调用`Eve`、`Adam`,最后是`God`合约。 -虽然`Eve`、`Adam`都是`God`的子合约,但整个过程中`God`合约只会被调用一次。原因是Solidity借鉴了Python的方式,强制一个由基类构成的DAG(有向无环图)使其保证一个特定的顺序。更多细节你可以查阅[Solidity的官方文档](https://solidity-cn.readthedocs.io/zh/develop/contracts.html?highlight=%E7%BB%A7%E6%89%BF#index-16)。 +虽然`Eve`、`Adam`都是`God`的子合约,但整个过程中`God`合约只会被调用一次。原因是`Solidity`借鉴了Python的方式,强制一个由基类构成的DAG(有向无环图)使其保证一个特定的顺序。更多细节你可以查阅[Solidity的官方文档](https://solidity-cn.readthedocs.io/zh/develop/contracts.html?highlight=%E7%BB%A7%E6%89%BF#index-16)。 ## 在Remix上验证 - 合约简单继承示例, 可以观察到Baba合约多了Yeye的函数 + ![13-1](./img/13-1.png) ![13-2](./img/13-2.png) - 合约多重继承可以参考简单继承的操作步骤来增加部署Erzi合约,然后观察暴露的函数以及尝试调用来查看日志 - 修饰器继承示例 + ![13-3](./img/13-3.png) ![13-4](./img/13-4.png) ![13-5](./img/13-5.png) - 构造函数继承示例 + ![13-6](./img/13-6.png) ![13-7](./img/13-7.png) - 调用父合约示例 + ![13-8](./img/13-8.png) ![13-9](./img/13-9.png) - * 菱形继承示例 +- 菱形继承示例 ![13-10](./img/13-10.png) ## 总结 -这一讲,我们介绍了`solidity`继承的基本用法,包括简单继承,多重继承,修饰器和构造函数的继承、调用父合约中的函数,以及多重继承中的菱形继承问题。 - +这一讲,我们介绍了`Solidity`继承的基本用法,包括简单继承,多重继承,修饰器和构造函数的继承、调用父合约中的函数,以及多重继承中的菱形继承问题。 diff --git a/14_Interface/readme.md b/14_Interface/readme.md index 952490db5..f80695430 100644 --- a/14_Interface/readme.md +++ b/14_Interface/readme.md @@ -10,7 +10,7 @@ tags: # WTF Solidity极简入门: 14. 抽象合约和接口 -我最近在重新学solidity,巩固一下细节,也写一个“WTF Solidity极简入门”,供小白们使用(编程大佬可以另找教程),每周更新1-3讲。 +我最近在重新学Solidity,巩固一下细节,也写一个“WTF Solidity极简入门”,供小白们使用(编程大佬可以另找教程),每周更新1-3讲。 推特:[@0xAA_Science](https://twitter.com/0xAA_Science) @@ -20,15 +20,18 @@ tags: ----- -这一讲,我们用`ERC721`的接口合约为例介绍`solidity`中的抽象合约(`abstract`)和接口(`interface`),帮助大家更好的理解`ERC721`标准。 +这一讲,我们用`ERC721`的接口合约为例介绍`Solidity`中的抽象合约(`abstract`)和接口(`interface`),帮助大家更好的理解`ERC721`标准。 ## 抽象合约 -如果一个智能合约里至少有一个未实现的函数,即某个函数缺少主体`{}`中的内容,则必须将该合约标为`abstract`,不然编译会报错;另外,未实现的函数需要加`virtual`,以便子合约重写。拿我们之前的[插入排序合约](https://github.com/AmazingAng/WTFSolidity/tree/main/07_InsertionSort)为例,如果我们还没想好具体怎么实现插入排序函数,那么可以把合约标为`abstract`,之后让别人补写上。 + +如果一个智能合约里至少有一个未实现的函数,即某个函数缺少主体`{}`中的内容,则必须将该合约标为`abstract`,不然编译会报错;另外,未实现的函数需要加`virtual`,以便子合约重写。拿我们之前的[插入排序合约](https://github.com/AmazingAng/WTFSolidity/tree/main/10_InsertionSort)为例,如果我们还没想好具体怎么实现插入排序函数,那么可以把合约标为`abstract`,之后让别人补写上。 + ```solidity abstract contract InsertionSort{ function insertionSort(uint[] memory a) public pure virtual returns(uint[] memory); } ``` + ## 接口 接口类似于抽象合约,但它不实现任何功能。接口的规则: @@ -45,7 +48,7 @@ abstract contract InsertionSort{ 2. 接口id(更多信息见[EIP165](https://eips.ethereum.org/EIPS/eip-165)) -另外,接口与合约`ABI`(Application Binary Interface)等价,可以相互转换:编译接口可以得到合约的`ABI`,利用[abi-to-sol工具](https://gnidan.github.io/abi-to-sol/)也可以将`ABI json`文件转换为`接口sol`文件。 +另外,接口与合约`ABI`(Application Binary Interface)等价,可以相互转换:编译接口可以得到合约的`ABI`,利用[abi-to-sol工具](https://gnidan.github.io/abi-to-sol/),也可以将`ABI json`文件转换为`接口sol`文件。 我们以`ERC721`接口合约`IERC721`为例,它定义了3个`event`和9个`function`,所有`ERC721`标准的NFT都实现了这些函数。我们可以看到,接口和常规合约的区别在于每个函数都以`;`代替函数体`{ }`结尾。 @@ -76,12 +79,15 @@ interface IERC721 is IERC165 { ``` ### IERC721事件 + `IERC721`包含3个事件,其中`Transfer`和`Approval`事件在`ERC20`中也有。 -- `Transfer`事件:在转账时被释放,记录代币的发出地址`from`,接收地址`to`和`tokenid`。 -- `Approval`事件:在授权时释放,记录授权地址`owner`,被授权地址`approved`和`tokenid`。 -- `ApprovalForAll`事件:在批量授权时释放,记录批量授权的发出地址`owner`,被授权地址`operator`和授权与否的`approved`。 + +- `Transfer`事件:在转账时被释放,记录代币的发出地址`from`,接收地址`to`和`tokenId`。 +- `Approval`事件:在授权时被释放,记录授权地址`owner`,被授权地址`approved`和`tokenId`。 +- `ApprovalForAll`事件:在批量授权时被释放,记录批量授权的发出地址`owner`,被授权地址`operator`和授权与否的`approved`。 ### IERC721函数 + - `balanceOf`:返回某地址的NFT持有量`balance`。 - `ownerOf`:返回某`tokenId`的主人`owner`。 - `transferFrom`:普通转账,参数为转出地址`from`,接收地址`to`和`tokenId`。 @@ -92,9 +98,8 @@ interface IERC721 is IERC165 { - `isApprovedForAll`:查询某地址的NFT是否批量授权给了另一个`operator`地址。 - `safeTransferFrom`:安全转账的重载函数,参数里面包含了`data`。 - - ### 什么时候使用接口? + 如果我们知道一个合约实现了`IERC721`接口,我们不需要知道它具体代码实现,就可以与它交互。 无聊猿`BAYC`属于`ERC721`代币,实现了`IERC721`接口的功能。我们不需要知道它的源代码,只需知道它的合约地址,用`IERC721`接口就可以与它交互,比如用`balanceOf()`来查询某个地址的`BAYC`余额,用`safeTransferFrom()`来转账`BAYC`。 @@ -117,11 +122,14 @@ contract interactBAYC { ``` ## 在Remix上验证 + - 抽象合约示例(简单的演示代码如图所示) + ![14-1](./img/14-1.png) - 接口示例(简单的演示代码如图所示) + ![14-2](./img/14-2.png) ## 总结 -这一讲,我介绍了`solidity`中的抽象合约(`abstract`)和接口(`interface`),他们都可以写模版并且减少代码冗余。我们还讲了`ERC721`接口合约`IERC721`,以及如何利用它与无聊猿`BAYC`合约进行交互。 +这一讲,我介绍了`Solidity`中的抽象合约(`abstract`)和接口(`interface`),他们都可以写模版并且减少代码冗余。我们还讲了`ERC721`接口合约`IERC721`,以及如何利用它与无聊猿`BAYC`合约进行交互。 diff --git a/15_Errors/readme.md b/15_Errors/readme.md index 7a423b12e..3699145ce 100644 --- a/15_Errors/readme.md +++ b/15_Errors/readme.md @@ -20,68 +20,82 @@ tags: ----- -这一讲,我们介绍`solidity`三种抛出异常的方法:`error`,`require`和`assert`,并比较三种方法的`gas`消耗。 +这一讲,我们介绍`Solidity`三种抛出异常的方法:`error`,`require`和`assert`,并比较三种方法的`gas`消耗。 ## 异常 -写智能合约经常会出`bug`,`solidity`中的异常命令帮助我们`debug`。 + +写智能合约经常会出`bug`,`Solidity`中的异常命令帮助我们`debug`。 ### Error + `error`是`solidity 0.8.4版本`新加的内容,方便且高效(省`gas`)地向用户解释操作失败的原因,同时还可以在抛出异常的同时携带参数,帮助开发者更好地调试。人们可以在`contract`之外定义异常。下面,我们定义一个`TransferNotOwner`异常,当用户不是代币`owner`的时候尝试转账,会抛出错误: + ```solidity error TransferNotOwner(); // 自定义error ``` + 我们也可以定义一个携带参数的异常,来提示尝试转账的账户地址 + ```solidity error TransferNotOwner(address sender); // 自定义的带参数的error ``` 在执行当中,`error`必须搭配`revert`(回退)命令使用。 + ```solidity - function transferOwner1(uint256 tokenId, address newOwner) public { - if(_owners[tokenId] != msg.sender){ - revert TransferNotOwner(); - // revert TransferNotOwner(msg.sender); - } - _owners[tokenId] = newOwner; +function transferOwner1(uint256 tokenId, address newOwner) public { + if(_owners[tokenId] != msg.sender){ + revert TransferNotOwner(); + // revert TransferNotOwner(msg.sender); } + _owners[tokenId] = newOwner; +} ``` + 我们定义了一个`transferOwner1()`函数,它会检查代币的`owner`是不是发起人,如果不是,就会抛出`TransferNotOwner`异常;如果是的话,就会转账。 ### Require + `require`命令是`solidity 0.8版本`之前抛出异常的常用方法,目前很多主流合约仍然还在使用它。它很好用,唯一的缺点就是`gas`随着描述异常的字符串长度增加,比`error`命令要高。使用方法:`require(检查条件,"异常的描述")`,当检查条件不成立的时候,就会抛出异常。 -我们用`require`命令重写一下上面的`transferOwner`函数: +我们用`require`命令重写一下上面的`transferOwner1`函数: + ```solidity - function transferOwner2(uint256 tokenId, address newOwner) public { - require(_owners[tokenId] == msg.sender, "Transfer Not Owner"); - _owners[tokenId] = newOwner; - } +function transferOwner2(uint256 tokenId, address newOwner) public { + require(_owners[tokenId] == msg.sender, "Transfer Not Owner"); + _owners[tokenId] = newOwner; +} ``` ### Assert + `assert`命令一般用于程序员写程序`debug`,因为它不能解释抛出异常的原因(比`require`少个字符串)。它的用法很简单,`assert(检查条件)`,当检查条件不成立的时候,就会抛出异常。 -我们用`assert`命令重写一下上面的`transferOwner`函数: +我们用`assert`命令重写一下上面的`transferOwner1`函数: + ```solidity - function transferOwner3(uint256 tokenId, address newOwner) public { - assert(_owners[tokenId] == msg.sender); - _owners[tokenId] = newOwner; - } +function transferOwner3(uint256 tokenId, address newOwner) public { + assert(_owners[tokenId] == msg.sender); + _owners[tokenId] = newOwner; +} ``` ## 在remix上验证 - + 1. 输入任意`uint256`数字和非0地址,调用`transferOwner1`,也就是`error`方法,控制台抛出了异常并显示我们自定义的`TransferNotOwner`。 - ![15 1.png](./img/15-1.png) - + + ![15-1.png](./img/15-1.png) + 2. 输入任意`uint256`数字和非0地址,调用`transferOwner2`,也就是`require`方法,控制台抛出了异常并打印出`require`中的字符串。 - ![15 2.png](./img/15-2.png) - + + ![15-2.png](./img/15-2.png) + 3. 输入任意`uint256`数字和非0地址,调用`transferOwner3`,也就是`assert`方法,控制台只抛出了异常。 - ![15 3.png](./img/15-3.png) - + + ![15-3.png](./img/15-3.png) ## 三种方法的gas比较 + 我们比较一下三种抛出异常的`gas`消耗,通过remix控制台的Debug按钮,能查到每次函数调用的`gas`消耗分别如下: (使用0.8.17版本编译) @@ -94,5 +108,5 @@ error TransferNotOwner(address sender); // 自定义的带参数的error **备注:** Solidity 0.8.0之前的版本,`assert`抛出的是一个 `panic exception`,会把剩余的 `gas` 全部消耗,不会返还。更多细节见[官方文档](https://docs.soliditylang.org/en/v0.8.17/control-structures.html)。 ## 总结 -这一讲,我们介绍`solidity`三种抛出异常的方法:`error`,`require`和`assert`,并比较了三种方法的`gas`消耗。结论:`error`既可以告知用户抛出异常的原因,又能省`gas`。 +这一讲,我们介绍`Solidity`三种抛出异常的方法:`error`,`require`和`assert`,并比较了三种方法的`gas`消耗。结论:`error`既可以告知用户抛出异常的原因,又能省`gas`。 diff --git a/16_Overloading/readme.md b/16_Overloading/readme.md index 331aa9009..ad934c986 100644 --- a/16_Overloading/readme.md +++ b/16_Overloading/readme.md @@ -8,7 +8,8 @@ tags: --- # WTF Solidity极简入门: 16. 函数重载 -我最近在重新学solidity,巩固一下细节,也写一个“WTF Solidity极简入门”,供小白们使用(编程大佬可以另找教程),每周更新1-3讲。 + +我最近在重新学Solidity,巩固一下细节,也写一个“WTF Solidity极简入门”,供小白们使用(编程大佬可以另找教程),每周更新1-3讲。 推特:[@0xAA_Science](https://twitter.com/0xAA_Science) @@ -19,8 +20,11 @@ tags: ----- ## 重载 -`solidity`中允许函数进行重载(`overloading`),即名字相同但输入参数类型不同的函数可以同时存在,他们被视为不同的函数。注意,`solidity`不允许修饰器(`modifier`)重载。 + +`Solidity`中允许函数进行重载(`overloading`),即名字相同但输入参数类型不同的函数可以同时存在,他们被视为不同的函数。注意,`Solidity`不允许修饰器(`modifier`)重载。 + ### 函数重载 + 举个例子,我们可以定义两个都叫`saySomething()`的函数,一个没有任何参数,输出`"Nothing"`;另一个接收一个`string`参数,输出这个`string`。 ```solidity @@ -36,24 +40,26 @@ function saySomething(string memory something) public pure returns(string memory 最终重载函数在经过编译器编译后,由于不同的参数类型,都变成了不同的函数选择器(selector)。关于函数选择器的具体内容可参考[WTF Solidity极简入门: 29. 函数选择器Selector](https://github.com/AmazingAng/WTFSolidity/tree/main/29_Selector)。 以 `Overloading.sol` 合约为例,在 Remix 上编译部署后,分别调用重载函数 `saySomething()` 和 `saySomething(string memory something)`,可以看到他们返回了不同的结果,被区分为不同的函数。 -![](./img/16-1.jpg) + +![16-1.jpg](./img/16-1.jpg) ### 实参匹配(Argument Matching) + 在调用重载函数时,会把输入的实际参数和函数参数的变量类型做匹配。 如果出现多个匹配的重载函数,则会报错。下面这个例子有两个叫`f()`的函数,一个参数为`uint8`,另一个为`uint256`: + ```solidity - function f(uint8 _in) public pure returns (uint8 out) { - out = _in; - } +function f(uint8 _in) public pure returns (uint8 out) { + out = _in; +} - function f(uint256 _in) public pure returns (uint256 out) { - out = _in; - } +function f(uint256 _in) public pure returns (uint256 out) { + out = _in; +} ``` + 我们调用`f(50)`,因为`50`既可以被转换为`uint8`,也可以被转换为`uint256`,因此会报错。 ## 总结 -这一讲,我们介绍了`solidity`中函数重载的基本用法:名字相同但输入参数类型不同的函数可以同时存在,他们被视为不同的函数。 - - +这一讲,我们介绍了`Solidity`中函数重载的基本用法:名字相同但输入参数类型不同的函数可以同时存在,他们被视为不同的函数。 diff --git a/17_Library/readme.md b/17_Library/readme.md index c519b140c..9e12e5c45 100644 --- a/17_Library/readme.md +++ b/17_Library/readme.md @@ -10,7 +10,7 @@ tags: # WTF Solidity极简入门: 17. 库合约 站在巨人的肩膀上 -我最近在重新学solidity,巩固一下细节,也写一个“WTF Solidity极简入门”,供小白们使用(编程大佬可以另找教程),每周更新1-3讲。 +我最近在重新学Solidity,巩固一下细节,也写一个“WTF Solidity极简入门”,供小白们使用(编程大佬可以另找教程),每周更新1-3讲。 推特:[@0xAA_Science](https://twitter.com/0xAA_Science) @@ -19,12 +19,11 @@ tags: 所有代码和教程开源在github: [github.com/AmazingAng/WTFSolidity](https://github.com/AmazingAng/WTFSolidity) ----- -这一讲,我们用`ERC721`的引用的库合约`String`为例介绍`solidity`中的库合约(`library`),并总结了常用的库合约。 +这一讲,我们用`ERC721`的引用的库合约`String`为例介绍`Solidity`中的库合约(`Library`),并总结了常用的库合约。 ## 库合约 -库合约是一种特殊的合约,为了提升`solidity`代码的复用性和减少`gas`而存在。库合约是一系列的函数合集,由大神或者项目方创作,咱们站在巨人的肩膀上,会用就行了。 - +库合约是一种特殊的合约,为了提升`Solidity`代码的复用性和减少`gas`而存在。库合约是一系列的函数合集,由大神或者项目方创作,咱们站在巨人的肩膀上,会用就行了。 ![库合约:站在巨人的肩膀上](https://images.mirror-media.xyz/publication-images/HJC0UjkALdrL8a2BmAE2J.jpeg?height=300&width=388) @@ -36,7 +35,9 @@ tags: 4. 不可以被销毁 ## String库合约 + `String库合约`是将`uint256`类型转换为相应的`string`类型的代码库,样例代码如下: + ```solidity library Strings { bytes16 private constant _HEX_SYMBOLS = "0123456789abcdef"; @@ -98,35 +99,42 @@ library Strings { } } ``` + 他主要包含两个函数,`toString()`将`uint256`转为`string`,`toHexString()`将`uint256`转换为`16进制`,在转换为`string`。 ### 如何使用库合约 -我们用String库合约的toHexString()来演示两种使用库合约中函数的办法。 -**1. 利用using for指令** +我们用`String`库合约的`toHexString()`来演示两种使用库合约中函数的办法。 -指令`using A for B;`可用于附加库合约(从库 A)到任何类型(B)。添加完指令后,库`A`中的函数会自动添加为`B`类型变量的成员,可以直接调用。注意:在调用的时候,这个变量会被当作第一个参数传递给函数: -```solidity +1. 利用using for指令 + + 指令`using A for B;`可用于附加库合约(从库 A)到任何类型(B)。添加完指令后,库`A`中的函数会自动添加为`B`类型变量的成员,可以直接调用。注意:在调用的时候,这个变量会被当作第一个参数传递给函数: + + ```solidity // 利用using for指令 using Strings for uint256; function getString1(uint256 _number) public pure returns(string memory){ // 库合约中的函数会自动添加为uint256型变量的成员 return _number.toHexString(); } -``` -**2. 通过库合约名称调用函数** -```solidity + ``` + +2. 通过库合约名称调用函数 + + ```solidity // 直接通过库合约名调用 function getString2(uint256 _number) public pure returns(string memory){ return Strings.toHexString(_number); } -``` -我们部署合约并输入`170`测试一下,两种方法均能返回正确的`16进制string` “0xaa”。证明我们调用库合约成功! + ``` +我们部署合约并输入`170`测试一下,两种方法均能返回正确的`16进制string` “0xaa”。证明我们调用库合约成功! ![成功调用库合约](https://images.mirror-media.xyz/publication-images/bzB_JDC9f5VWHRjsjQyQa.png?height=750&width=580) + ## 总结 -这一讲,我们用`ERC721`的引用的库合约`String`为例介绍`solidity`中的库合约(`Library`)。99%的开发者都不需要自己去写库合约,会用大神写的就可以了。我们只需要知道什么情况该用什么库合约。常用的有: + +这一讲,我们用`ERC721`的引用的库合约`String`为例介绍`Solidity`中的库合约(`Library`)。99%的开发者都不需要自己去写库合约,会用大神写的就可以了。我们只需要知道什么情况该用什么库合约。常用的有: 1. [String](https://github.com/OpenZeppelin/openzeppelin-contracts/blob/4a9cc8b4918ef3736229a5cc5a310bdc17bf759f/contracts/utils/Strings.sol):将`uint256`转换为`String` 2. [Address](https://github.com/OpenZeppelin/openzeppelin-contracts/blob/4a9cc8b4918ef3736229a5cc5a310bdc17bf759f/contracts/utils/Address.sol):判断某个地址是否为合约地址 diff --git a/18_Import/readme.md b/18_Import/readme.md index a45fcb7fc..49efb0502 100644 --- a/18_Import/readme.md +++ b/18_Import/readme.md @@ -9,13 +9,13 @@ tags: # WTF Solidity极简入门: 18. Import -我最近在重新学solidity,巩固一下细节,也写一个“WTF Solidity极简入门”,供小白们使用(编程大佬可以另找教程),每周更新1-3讲。 +我最近在重新学Solidity,巩固一下细节,也写一个“WTF Solidity极简入门”,供小白们使用(编程大佬可以另找教程),每周更新1-3讲。 -欢迎关注我的推特:[@0xAA_Science](https://twitter.com/0xAA_Science) +推特:[@0xAA_Science](https://twitter.com/0xAA_Science) -欢迎加入WTF科学家社区,内有加微信群方法:[链接](https://discord.gg/5akcruXrsk) +社区:[Discord](https://discord.gg/5akcruXrsk)|[微信群](https://docs.google.com/forms/d/e/1FAIpQLSe4KGT8Sh6sJ7hedQRuIYirOoZK_85miz3dw7vA1-YjodgJ-A/viewform?usp=sf_link)|[官网 wtf.academy](https://wtf.academy) -所有代码和教程开源在github(1024个star发课程认证,2048个star发社群NFT): [github.com/AmazingAng/WTFSolidity](https://github.com/AmazingAng/WTFSolidity) +所有代码和教程开源在github: [github.com/AmazingAng/WTFSolidity](https://github.com/AmazingAng/WTFSolidity) ----- @@ -25,37 +25,53 @@ tags: - 通过源文件相对位置导入,例子: -``` -文件结构 -├── Import.sol -└── Yeye.sol + ```text + 文件结构 + ├── Import.sol + └── Yeye.sol -// 通过文件相对位置import -import './Yeye.sol'; -``` + // 通过文件相对位置import + import './Yeye.sol'; + ``` - 通过源文件网址导入网上的合约的全局符号,例子: -``` -// 通过网址引用 -import 'https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/utils/Address.sol'; -``` + + ```text + // 通过网址引用 + import 'https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/utils/Address.sol'; + ``` - 通过`npm`的目录导入,例子: -```solidity -import '@openzeppelin/contracts/access/Ownable.sol'; -``` + + ```solidity + import '@openzeppelin/contracts/access/Ownable.sol'; + ``` - 通过指定`全局符号`导入合约特定的全局符号,例子: -```solidity -import {Yeye} from './Yeye.sol'; -``` + + ```solidity + import {Yeye} from './Yeye.sol'; + ``` - 引用(`import`)在代码中的位置为:在声明版本号之后,在其余代码之前。 ## 测试导入结果 我们可以用下面这段代码测试是否成功导入了外部源代码: + ```solidity +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.4; + +// 通过文件相对位置import +import './Yeye.sol'; +// 通过`全局符号`导入特定的合约 +import {Yeye} from './Yeye.sol'; +// 通过网址引用 +import 'https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/utils/Address.sol'; +// 引用oppenzepplin合约 +import '@openzeppelin/contracts/access/Ownable.sol'; + contract Import { // 成功导入Address库 using Address for address; @@ -69,7 +85,8 @@ contract Import { } ``` -![result](https://github.com/AmazingAng/WTF-Solidity/blob/main/Languages/en/18_Import_en/img/18-1.png) +![result](./img/18-1.png) ## 总结 + 这一讲,我们介绍了利用`import`关键字导入外部源代码的方法。通过`import`关键字,可以引用我们写的其他文件中的合约或者函数,也可以直接导入别人写好的代码,非常方便。 diff --git a/19_Fallback/readme.md b/19_Fallback/readme.md index 8cac1d3e0..348819d2d 100644 --- a/19_Fallback/readme.md +++ b/19_Fallback/readme.md @@ -10,7 +10,7 @@ tags: # WTF Solidity极简入门: 19. 接收ETH receive和fallback -我最近在重新学solidity,巩固一下细节,也写一个“WTF Solidity极简入门”,供小白们使用(编程大佬可以另找教程),每周更新1-3讲。 +我最近在重新学Solidity,巩固一下细节,也写一个“WTF Solidity极简入门”,供小白们使用(编程大佬可以另找教程),每周更新1-3讲。 推特:[@0xAA_Science](https://twitter.com/0xAA_Science) @@ -21,48 +21,54 @@ tags: ----- `Solidity`支持两种特殊的回调函数,`receive()`和`fallback()`,他们主要在两种情况下被使用: + 1. 接收ETH 2. 处理合约中不存在的函数调用(代理合约proxy contract) -注意⚠️:在solidity 0.6.x版本之前,语法上只有 `fallback()` 函数,用来接收用户发送的ETH时调用以及在被调用函数签名没有匹配到时,来调用。 -0.6版本之后,solidity才将 `fallback()` 函数拆分成 `receive()` 和 `fallback()` 两个函数。 +注意⚠️:在Solidity 0.6.x版本之前,语法上只有 `fallback()` 函数,用来接收用户发送的ETH时调用以及在被调用函数签名没有匹配到时,来调用。 +0.6版本之后,Solidity才将 `fallback()` 函数拆分成 `receive()` 和 `fallback()` 两个函数。 我们这一讲主要讲接收ETH的情况。 ## 接收ETH函数 receive + `receive()`函数是在合约收到`ETH`转账时被调用的函数。一个合约最多有一个`receive()`函数,声明方式与一般函数不一样,不需要`function`关键字:`receive() external payable { ... }`。`receive()`函数不能有任何的参数,不能返回任何值,必须包含`external`和`payable`。 当合约接收ETH的时候,`receive()`会被触发。`receive()`最好不要执行太多的逻辑因为如果别人用`send`和`transfer`方法发送`ETH`的话,`gas`会限制在`2300`,`receive()`太复杂可能会触发`Out of Gas`报错;如果用`call`就可以自定义`gas`执行更复杂的逻辑(这三种发送ETH的方法我们之后会讲到)。 我们可以在`receive()`里发送一个`event`,例如: + ```solidity - // 定义事件 - event Received(address Sender, uint Value); - // 接收ETH时释放Received事件 - receive() external payable { - emit Received(msg.sender, msg.value); - } +// 定义事件 +event Received(address Sender, uint Value); +// 接收ETH时释放Received事件 +receive() external payable { + emit Received(msg.sender, msg.value); +} ``` 有些恶意合约,会在`receive()` 函数(老版本的话,就是 `fallback()` 函数)嵌入恶意消耗`gas`的内容或者使得执行故意失败的代码,导致一些包含退款和转账逻辑的合约不能正常工作,因此写包含退款等逻辑的合约时候,一定要注意这种情况。 ## 回退函数 fallback + `fallback()`函数会在调用合约不存在的函数时被触发。可用于接收ETH,也可以用于代理合约`proxy contract`。`fallback()`声明时不需要`function`关键字,必须由`external`修饰,一般也会用`payable`修饰,用于接收ETH:`fallback() external payable { ... }`。 我们定义一个`fallback()`函数,被触发时候会释放`fallbackCalled`事件,并输出`msg.sender`,`msg.value`和`msg.data`: ```solidity - event fallbackCalled(address Sender, uint Value, bytes Data); +event fallbackCalled(address Sender, uint Value, bytes Data); - // fallback - fallback() external payable{ - emit fallbackCalled(msg.sender, msg.value, msg.data); - } +// fallback +fallback() external payable{ + emit fallbackCalled(msg.sender, msg.value, msg.data); +} ``` ## receive和fallback的区别 + `receive`和`fallback`都能够用于接收`ETH`,他们触发的规则如下: -``` + +```text 触发fallback() 还是 receive()? 接收ETH | @@ -76,26 +82,27 @@ receive()存在? fallback() / \ receive() fallback() ``` + 简单来说,合约接收`ETH`时,`msg.data`为空且存在`receive()`时,会触发`receive()`;`msg.data`不为空或不存在`receive()`时,会触发`fallback()`,此时`fallback()`必须为`payable`。 `receive()`和`payable fallback()`均不存在的时候,向合约**直接**发送`ETH`将会报错(你仍可以通过带有`payable`的函数向合约发送`ETH`)。 - ## Remix 演示 + 1. 首先在 Remix 上部署合约 "Fallback.sol"。 2. "VALUE" 栏中填入要发送给合约的金额(单位是 Wei),然后点击 "Transact"。 - ![](img/19-1.jpg) + ![19-1.jpg](img/19-1.jpg) 3. 可以看到交易成功,并且触发了 "receivedCalled" 事件。 - ![](img/19-2.jpg) + ![19-2.jpg](img/19-2.jpg) 4. "VALUE" 栏中填入要发送给合约的金额(单位是 Wei),"CALLDATA" 栏中填入随意编写的`msg.data`,然后点击 "Transact"。 - ![](img/19-3.jpg) - + + ![19-3.jpg](img/19-3.jpg) 5. 可以看到交易成功,并且触发了 "fallbackCalled" 事件。 - ![](img/19-4.jpg) + ![19-4.jpg](img/19-4.jpg) ## 总结 -这一讲,我介绍了`Solidity`中的两种特殊函数,`receive()`和`fallback()`,他们主要在两种情况下被使用,他们主要用于处理接收`ETH`和代理合约`proxy contract`。 +这一讲,我介绍了`Solidity`中的两种特殊函数,`receive()`和`fallback()`,他们主要在两种情况下被使用,他们主要用于处理接收`ETH`和代理合约`proxy contract`。 diff --git a/20_SendETH/readme.md b/20_SendETH/readme.md index a22aeb902..dfc0b9aac 100644 --- a/20_SendETH/readme.md +++ b/20_SendETH/readme.md @@ -9,7 +9,7 @@ tags: # WTF Solidity极简入门: 20. 发送ETH -我最近在重新学solidity,巩固一下细节,也写一个“WTF Solidity极简入门”,供小白们使用(编程大佬可以另找教程),每周更新1-3讲。 +我最近在重新学Solidity,巩固一下细节,也写一个“WTF Solidity极简入门”,供小白们使用(编程大佬可以另找教程),每周更新1-3讲。 推特:[@0xAA_Science](https://twitter.com/0xAA_Science) @@ -21,7 +21,9 @@ tags: `Solidity`有三种方法向其他合约发送`ETH`,他们是:`transfer()`,`send()`和`call()`,其中`call()`是被鼓励的用法。 ## 接收ETH合约 + 我们先部署一个接收`ETH`合约`ReceiveETH`。`ReceiveETH`合约里有一个事件`Log`,记录收到的`ETH`数量和`gas`剩余。还有两个函数,一个是`receive()`函数,收到`ETH`被触发,并发送`Log`事件;另一个是查询合约`ETH`余额的`getBalance()`函数。 + ```solidity contract ReceiveETH { // 收到eth事件,记录amount和gas @@ -44,7 +46,9 @@ contract ReceiveETH { ![20-1](./img/20-1.png) ## 发送ETH合约 + 我们将实现三种方法向`ReceiveETH`合约发送`ETH`。首先,先在发送ETH合约`SendETH`中实现`payable`的`构造函数`和`receive()`,让我们能够在部署时和部署后向合约转账。 + ```solidity contract SendETH { // 构造函数,payable使得部署的时候可以转eth进去 @@ -53,16 +57,19 @@ contract SendETH { receive() external payable{} } ``` + ### transfer + - 用法是`接收方地址.transfer(发送ETH数额)`。 - `transfer()`的`gas`限制是`2300`,足够用于转账,但对方合约的`fallback()`或`receive()`函数不能实现太复杂的逻辑。 - `transfer()`如果转账失败,会自动`revert`(回滚交易)。 代码样例,注意里面的`_to`填`ReceiveETH`合约的地址,`amount`是`ETH`转账金额: + ```solidity // 用transfer()发送ETH function transferETH(address payable _to, uint256 amount) external payable{ - _to.transfer(amount); + _to.transfer(amount); } ``` @@ -86,13 +93,16 @@ function transferETH(address payable _to, uint256 amount) external payable{ - `send()`的返回值是`bool`,代表着转账成功或失败,需要额外代码处理一下。 代码样例: + ```solidity +error SendFailed(); // 用send发送ETH失败error + // send()发送ETH function sendETH(address payable _to, uint256 amount) external payable{ // 处理下send的返回值,如果失败,revert交易并发送error bool success = _to.send(amount); if(!success){ - revert SendFailed(); + revert SendFailed(); } } ``` @@ -113,13 +123,16 @@ function sendETH(address payable _to, uint256 amount) external payable{ - `call()`的返回值是`(bool, bytes)`,其中`bool`代表着转账成功或失败,需要额外代码处理一下。 代码样例: + ```solidity +error CallFailed(); // 用call发送ETH失败error + // call()发送ETH function callETH(address payable _to, uint256 amount) external payable{ // 处理下call的返回值,如果失败,revert交易并发送error (bool success,) = _to.call{value: amount}(""); if(!success){ - revert CallFailed(); + revert CallFailed(); } } ``` @@ -135,10 +148,9 @@ function callETH(address payable _to, uint256 amount) external payable{ 运行三种方法,可以看到,他们都可以成功地向`ReceiveETH`合约发送`ETH`。 ## 总结 -这一讲,我们介绍`solidity`三种发送`ETH`的方法:`transfer`,`send`和`call`。 + +这一讲,我们介绍`Solidity`三种发送`ETH`的方法:`transfer`,`send`和`call`。 + - `call`没有`gas`限制,最为灵活,是最提倡的方法; - `transfer`有`2300 gas`限制,但是发送失败会自动`revert`交易,是次优选择; - `send`有`2300 gas`限制,而且发送失败不会自动`revert`交易,几乎没有人用它。 - - - diff --git a/21_CallContract/readme.md b/21_CallContract/readme.md index 3865b1c23..b8f7af782 100644 --- a/21_CallContract/readme.md +++ b/21_CallContract/readme.md @@ -9,7 +9,7 @@ tags: # WTF Solidity极简入门: 21. 调用其他合约 -我最近在重新学solidity,巩固一下细节,也写一个“WTF Solidity极简入门”,供小白们使用(编程大佬可以另找教程),每周更新1-3讲。 +我最近在重新学Solidity,巩固一下细节,也写一个“WTF Solidity极简入门”,供小白们使用(编程大佬可以另找教程),每周更新1-3讲。 推特:[@0xAA_Science](https://twitter.com/0xAA_Science) @@ -21,9 +21,10 @@ tags: ## 调用已部署合约 -在Solidity中,一个合约可以调用另一个合约的函数,这在构建复杂的DApps时非常有用。本教程将会介绍如何在已知合约代码(或接口)和地址的情况下,调用已部署的合约。 +在`Solidity`中,一个合约可以调用另一个合约的函数,这在构建复杂的DApps时非常有用。本教程将会介绍如何在已知合约代码(或接口)和地址的情况下,调用已部署的合约。 ## 目标合约 + 我们先写一个简单的合约`OtherContract`,用于被其他合约调用。 ```solidity @@ -54,11 +55,13 @@ contract OtherContract { ``` 这个合约包含一个状态变量`_x`,一个事件`Log`在收到`ETH`时触发,三个函数: + - `getBalance()`: 返回合约`ETH`余额。 - `setX()`: `external payable`函数,可以设置`_x`的值,并向合约发送`ETH`。 - `getX()`: 读取`_x`的值。 ## 调用`OtherContract`合约 + 我们可以利用合约的地址和合约代码(或接口)来创建合约的引用:`_Name(_Address)`,其中`_Name`是合约名,应与合约代码(或接口)中标注的合约名保持一致,`_Address`是合约地址。然后用合约的引用来调用它的函数:`_Name(_Address).f()`,其中`f()`是要调用的函数。 下面我们介绍4个调用合约的例子,在remix中编译合约后,分别部署`OtherContract`和`CallContract`: @@ -70,12 +73,13 @@ contract OtherContract { ![deploy contract2 in remix](./img/21-3.png) ### 1. 传入合约地址 + 我们可以在函数里传入目标合约地址,生成目标合约的引用,然后调用目标函数。以调用`OtherContract`合约的`setX`函数为例,我们在新合约中写一个`callSetX`函数,传入已部署好的`OtherContract`合约地址`_Address`和`setX`的参数`x`: ```solidity - function callSetX(address _Address, uint256 x) external{ - OtherContract(_Address).setX(x); - } +function callSetX(address _Address, uint256 x) external{ + OtherContract(_Address).setX(x); +} ``` 复制`OtherContract`合约的地址,填入`callSetX`函数的参数中,成功调用后,调用`OtherContract`合约中的`getX`验证`x`变为123 @@ -85,14 +89,15 @@ contract OtherContract { ![call contract2 in remix](./img/21-5.png) ### 2. 传入合约变量 + 我们可以直接在函数里传入合约的引用,只需要把上面参数的`address`类型改为目标合约名,比如`OtherContract`。下面例子实现了调用目标合约的`getX()`函数。 -**注意**该函数参数`OtherContract _Address`底层类型仍然是`address`,生成的`ABI`中、调用`callGetX`时传入的参数都是`address`类型 +**注意**:该函数参数`OtherContract _Address`底层类型仍然是`address`,生成的`ABI`中、调用`callGetX`时传入的参数都是`address`类型 ```solidity - function callGetX(OtherContract _Address) external view returns(uint x){ - x = _Address.getX(); - } +function callGetX(OtherContract _Address) external view returns(uint x){ + x = _Address.getX(); +} ``` 复制`OtherContract`合约的地址,填入`callGetX`函数的参数中,调用后成功获取`x`的值 @@ -100,12 +105,14 @@ contract OtherContract { ![call contract3 in remix](./img/21-6.png) ### 3. 创建合约变量 + 我们可以创建合约变量,然后通过它来调用目标函数。下面例子,我们给变量`oc`存储了`OtherContract`合约的引用: + ```solidity - function callGetX2(address _Address) external view returns(uint x){ - OtherContract oc = OtherContract(_Address); - x = oc.getX(); - } +function callGetX2(address _Address) external view returns(uint x){ + OtherContract oc = OtherContract(_Address); + x = oc.getX(); +} ``` 复制`OtherContract`合约的地址,填入`callGetX2`函数的参数中,调用后成功获取`x`的值 @@ -113,13 +120,15 @@ contract OtherContract { ![call contract4 in remix](./img/21-7.png) ### 4. 调用合约并发送`ETH` + 如果目标合约的函数是`payable`的,那么我们可以通过调用它来给合约转账:`_Name(_Address).f{value: _Value}()`,其中`_Name`是合约名,`_Address`是合约地址,`f`是目标函数名,`_Value`是要转的`ETH`数额(以`wei`为单位)。 `OtherContract`合约的`setX`函数是`payable`的,在下面这个例子中我们通过调用`setX`来往目标合约转账。 + ```solidity - function setXTransferETH(address otherContract, uint256 x) payable external{ - OtherContract(otherContract).setX{value: msg.value}(x); - } +function setXTransferETH(address otherContract, uint256 x) payable external{ + OtherContract(otherContract).setX{value: msg.value}(x); +} ``` 复制`OtherContract`合约的地址,填入`setXTransferETH`函数的参数中,并转入10ETH @@ -131,4 +140,5 @@ contract OtherContract { ![call contract6 in remix](./img/21-9.png) ## 总结 + 这一讲,我们介绍了如何通过目标合约代码(或接口)和地址来创建合约的引用,从而调用目标合约的函数。 diff --git a/22_Call/readme.md b/22_Call/readme.md index 5d01c86ef..39554cee2 100644 --- a/22_Call/readme.md +++ b/22_Call/readme.md @@ -10,7 +10,7 @@ tags: # WTF Solidity极简入门: 22. Call -我最近在重新学solidity,巩固一下细节,也写一个“WTF Solidity极简入门”,供小白们使用(编程大佬可以另找教程),每周更新1-3讲。 +我最近在重新学Solidity,巩固一下细节,也写一个“WTF Solidity极简入门”,供小白们使用(编程大佬可以另找教程),每周更新1-3讲。 推特:[@0xAA_Science](https://twitter.com/0xAA_Science) @@ -23,32 +23,39 @@ tags: 我们曾在[第20讲:发送ETH](https://github.com/AmazingAng/WTFSolidity/tree/main/20_SendETH)那一讲介绍过利用`call`来发送`ETH`,这一讲我们将介绍如何利用它调用合约。 ## Call + `call` 是`address`类型的低级成员函数,它用来与其他合约交互。它的返回值为`(bool, bytes memory)`,分别对应`call`是否成功以及目标函数的返回值。 -- `call`是`solidity`官方推荐的通过触发`fallback`或`receive`函数发送`ETH`的方法。 +- `call`是`Solidity`官方推荐的通过触发`fallback`或`receive`函数发送`ETH`的方法。 - 不推荐用`call`来调用另一个合约,因为当你调用不安全合约的函数时,你就把主动权交给了它。推荐的方法仍是声明合约变量后调用函数,见[第21讲:调用其他合约](https://github.com/AmazingAng/WTFSolidity/tree/main/21_CallContract) - 当我们不知道对方合约的源代码或`ABI`,就没法生成合约变量;这时,我们仍可以通过`call`调用对方合约的函数。 ### `call`的使用规则 + `call`的使用规则如下: -``` + +```text 目标合约地址.call(字节码); ``` + 其中`字节码`利用结构化编码函数`abi.encodeWithSignature`获得: -``` + +```text abi.encodeWithSignature("函数签名", 逗号分隔的具体参数) ``` -`函数签名`为`"函数名(逗号分隔的参数类型)"`。例如`abi.encodeWithSignature("f(uint256,address)", _x, _addr)`。 -另外`call`在调用合约时可以指定交易发送的`ETH`数额和`gas`: +`函数签名`为`"函数名(逗号分隔的参数类型)"`。例如`abi.encodeWithSignature("f(uint256,address)", _x, _addr)`。 -``` +另外`call`在调用合约时可以指定交易发送的`ETH`数额和`gas`数额: + +```text 目标合约地址.call{value:发送数额, gas:gas数额}(字节码); ``` 看起来有点复杂,下面我们举个`call`应用的例子。 ### 目标合约 + 我们先写一个简单的目标合约`OtherContract`并部署,代码与第21讲中基本相同,只是多了`fallback`函数。 ```solidity @@ -81,12 +88,14 @@ contract OtherContract { ``` 这个合约包含一个状态变量`x`,一个在收到`ETH`时触发的事件`Log`,三个函数: + - `getBalance()`: 返回合约`ETH`余额。 - `setX()`: `external payable`函数,可以设置`x`的值,并向合约发送`ETH`。 - `getX()`: 读取`x`的值。 ### 利用`call`调用目标合约 -**1. Response事件** + +#### 1. Response事件 我们写一个`Call`合约来调用目标合约函数。首先定义一个`Response`事件,输出`call`返回的`success`和`data`,方便我们观察返回值。 @@ -95,18 +104,18 @@ contract OtherContract { event Response(bool success, bytes data); ``` -**2. 调用setX函数** +#### 2. 调用setX函数 我们定义`callSetX`函数来调用目标合约的`setX()`,转入`msg.value`数额的`ETH`,并释放`Response`事件输出`success`和`data`: ```solidity function callSetX(address payable _addr, uint256 x) public payable { - // call setX(),同时可以发送ETH - (bool success, bytes memory data) = _addr.call{value: msg.value}( - abi.encodeWithSignature("setX(uint256)", x) - ); + // call setX(),同时可以发送ETH + (bool success, bytes memory data) = _addr.call{value: msg.value}( + abi.encodeWithSignature("setX(uint256)", x) + ); - emit Response(success, data); //释放事件 + emit Response(success, data); //释放事件 } ``` @@ -114,38 +123,38 @@ function callSetX(address payable _addr, uint256 x) public payable { ![22-1](./img/22-1.png) -**3. 调用getX函数** +#### 3. 调用getX函数 下面我们调用`getX()`函数,它将返回目标合约`_x`的值,类型为`uint256`。我们可以利用`abi.decode`来解码`call`的返回值`data`,并读出数值。 ```solidity function callGetX(address _addr) external returns(uint256){ - // call getX() - (bool success, bytes memory data) = _addr.call( - abi.encodeWithSignature("getX()") - ); + // call getX() + (bool success, bytes memory data) = _addr.call( + abi.encodeWithSignature("getX()") + ); - emit Response(success, data); //释放事件 - return abi.decode(data, (uint256)); + emit Response(success, data); //释放事件 + return abi.decode(data, (uint256)); } ``` + 从`Response`事件的输出,我们可以看到`data`为`0x0000000000000000000000000000000000000000000000000000000000000005`。而经过`abi.decode`,最终返回值为`5`。 ![22-2](./img/22-2.png) -**4. 调用不存在的函数** +#### 4. 调用不存在的函数 如果我们给`call`输入的函数不存在于目标合约,那么目标合约的`fallback`函数会被触发。 - ```solidity function callNonExist(address _addr) external{ - // call 不存在的函数 - (bool success, bytes memory data) = _addr.call( - abi.encodeWithSignature("foo(uint256)") - ); + // call 不存在的函数 + (bool success, bytes memory data) = _addr.call( + abi.encodeWithSignature("foo(uint256)") + ); - emit Response(success, data); //释放事件 + emit Response(success, data); //释放事件 } ``` @@ -156,4 +165,3 @@ function callNonExist(address _addr) external{ ## 总结 这一讲,我们介绍了如何用`call`这一低级函数来调用其他合约。`call`不是调用合约的推荐方法,因为不安全。但他能让我们在不知道源代码和`ABI`的情况下调用目标合约,很有用。 - diff --git a/23_Delegatecall/readme.md b/23_Delegatecall/readme.md index 29527efec..29108b636 100644 --- a/23_Delegatecall/readme.md +++ b/23_Delegatecall/readme.md @@ -10,7 +10,7 @@ tags: # WTF Solidity极简入门: 23. Delegatecall -我最近在重新学solidity,巩固一下细节,也写一个“WTF Solidity极简入门”,供小白们使用(编程大佬可以另找教程),每周更新1-3讲。 +我最近在重新学Solidity,巩固一下细节,也写一个“WTF Solidity极简入门”,供小白们使用(编程大佬可以另找教程),每周更新1-3讲。 推特:[@0xAA_Science](https://twitter.com/0xAA_Science) @@ -20,8 +20,9 @@ tags: ----- -## `delegatecall` -`delegatecall`与`call`类似,是`solidity`中地址类型的低级成员函数。`delegate`中是委托/代表的意思,那么`delegatecall`委托了什么? +## `Delegatecall` + +`delegatecall`与`call`类似,是`Solidity`中地址类型的低级成员函数。`delegate`中是委托/代表的意思,那么`delegatecall`委托了什么? 当用户`A`通过合约`B`来`call`合约`C`的时候,执行的是合约`C`的函数,`上下文`(`Context`,可以理解为包含变量和状态的环境)也是合约`C`的:`msg.sender`是`B`的地址,并且如果函数改变一些状态变量,产生的效果会作用于合约`C`的变量上。 @@ -34,20 +35,25 @@ tags: 大家可以这样理解:一个投资者(用户`A`)把他的资产(`B`合约的`状态变量`)都交给一个风险投资代理(`C`合约)来打理。执行的是风险投资代理的函数,但是改变的是资产的状态。 `delegatecall`语法和`call`类似,也是: + ```solidity 目标合约地址.delegatecall(二进制编码); ``` + 其中`二进制编码`利用结构化编码函数`abi.encodeWithSignature`获得: + ```solidity abi.encodeWithSignature("函数签名", 逗号分隔的具体参数) ``` -`函数签名`为`"函数名(逗号分隔的参数类型)"`。例如`abi.encodeWithSignature("f(uint256,address)", _x, _addr)`。 + +`函数签名`为`"函数名(逗号分隔的参数类型)"`。例如`abi.encodeWithSignature("f(uint256,address)", _x, _addr)`。 和`call`不一样,`delegatecall`在调用合约时可以指定交易发送的`gas`,但不能指定发送的`ETH`数额 > **注意**:`delegatecall`有安全隐患,使用时要保证当前合约和目标合约的状态变量存储结构相同,并且目标合约安全,不然会造成资产损失。 ## 什么情况下会用到`delegatecall`? + 目前`delegatecall`主要有两个应用场景: 1. 代理合约(`Proxy Contract`):将智能合约的存储合约和逻辑合约分开:代理合约(`Proxy Contract`)存储所有相关的变量,并且保存逻辑合约的地址;所有函数存在逻辑合约(`Logic Contract`)里,通过`delegatecall`执行。当升级时,只需要将代理合约指向新的逻辑合约即可。 @@ -55,9 +61,13 @@ abi.encodeWithSignature("函数签名", 逗号分隔的具体参数) 2. EIP-2535 Diamonds(钻石):钻石是一个支持构建可在生产中扩展的模块化智能合约系统的标准。钻石是具有多个实施合约的代理合约。 更多信息请查看:[钻石标准简介](https://eip2535diamonds.substack.com/p/introduction-to-the-diamond-standard)。 ## `delegatecall`例子 + 调用结构:你(`A`)通过合约`B`调用目标合约`C`。 + ### 被调用的合约C + 我们先写一个简单的目标合约`C`:有两个`public`变量:`num`和`sender`,分别是`uint256`和`address`类型;有一个函数,可以将`num`设定为传入的`_num`,并且将`sender`设为`msg.sender`。 + ```solidity // 被调用的合约C contract C { @@ -70,66 +80,67 @@ contract C { } } ``` + ### 发起调用的合约B + 首先,合约`B`必须和目标合约`C`的变量存储布局必须相同,两个变量,并且顺序为`num`和`sender` + ```solidity contract B { uint public num; address public sender; +} ``` 接下来,我们分别用`call`和`delegatecall`来调用合约`C`的`setVars`函数,更好的理解它们的区别。 `callSetVars`函数通过`call`来调用`setVars`。它有两个参数`_addr`和`_num`,分别对应合约`C`的地址和`setVars`的参数。 + ```solidity - // 通过call来调用C的setVars()函数,将改变合约C里的状态变量 - function callSetVars(address _addr, uint _num) external payable{ - // call setVars() - (bool success, bytes memory data) = _addr.call( - abi.encodeWithSignature("setVars(uint256)", _num) - ); - } +// 通过call来调用C的setVars()函数,将改变合约C里的状态变量 +function callSetVars(address _addr, uint _num) external payable{ + // call setVars() + (bool success, bytes memory data) = _addr.call( + abi.encodeWithSignature("setVars(uint256)", _num) + ); +} ``` 而`delegatecallSetVars`函数通过`delegatecall`来调用`setVars`。与上面的`callSetVars`函数相同,有两个参数`_addr`和`_num`,分别对应合约`C`的地址和`setVars`的参数。 ```solidity - // 通过delegatecall来调用C的setVars()函数,将改变合约B里的状态变量 - function delegatecallSetVars(address _addr, uint _num) external payable{ - // delegatecall setVars() - (bool success, bytes memory data) = _addr.delegatecall( - abi.encodeWithSignature("setVars(uint256)", _num) - ); - } +// 通过delegatecall来调用C的setVars()函数,将改变合约B里的状态变量 +function delegatecallSetVars(address _addr, uint _num) external payable{ + // delegatecall setVars() + (bool success, bytes memory data) = _addr.delegatecall( + abi.encodeWithSignature("setVars(uint256)", _num) + ); } ``` ### 在remix上验证 -1. 首先,我们把合约`B`和`C`都部署好 - -![deploy.png](./img/23-1.png) +1. 首先,我们把合约`B`和`C`都部署好 + ![deploy.png](./img/23-1.png) 2. 部署之后,查看`C`合约状态变量的初始值,`B`合约的状态变量也是一样。 -![initialstate.png](./img/23-2.png) - + ![initialstate.png](./img/23-2.png) 3. 此时,调用合约`B`中的`callSetVars`,传入参数为合约`C`地址和`10` -![call.png](./img/23-3.png) + ![call.png](./img/23-3.png) 4. 运行后,合约`C`中的状态变量将被修改:`num`被改为`10`,`sender`变为合约`B`的地址 -![resultcall.png](./img/23-4.png) - - + ![resultcall.png](./img/23-4.png) 5. 接下来,我们调用合约`B`中的`delegatecallSetVars`,传入参数为合约`C`地址和`100` -![delegatecall.png](./img/23-5.png) + ![delegatecall.png](./img/23-5.png) 6. 由于是`delegatecall`,上下文为合约`B`。在运行后,合约`B`中的状态变量将被修改:`num`被改为`100`,`sender`变为你的钱包地址。合约`C`中的状态变量不会被修改。 -![resultdelegatecall.png](./img/23-6.png) + ![resultdelegatecall.png](./img/23-6.png) ## 总结 -这一讲我们介绍了`solidity`中的另一个低级函数`delegatecall`。与`call`类似,它可以用来调用其他合约;不同点在于运行的上下文,`B call C`,上下文为`C`;而`B delegatecall C`,上下文为`B`。目前`delegatecall`最大的应用是代理合约和`EIP-2535 Diamonds`(钻石)。 + +这一讲我们介绍了`Solidity`中的另一个低级函数`delegatecall`。与`call`类似,它可以用来调用其他合约;不同点在于运行的上下文,`B call C`,上下文为`C`;而`B delegatecall C`,上下文为`B`。目前`delegatecall`最大的应用是代理合约和`EIP-2535 Diamonds`(钻石)。 diff --git a/24_Create/readme.md b/24_Create/readme.md index 7ee1b828c..8420d3e3b 100644 --- a/24_Create/readme.md +++ b/24_Create/readme.md @@ -9,7 +9,7 @@ tags: # WTF Solidity极简入门: 24. 在合约中创建新合约 -我最近在重新学solidity,巩固一下细节,也写一个“WTF Solidity极简入门”,供小白们使用(编程大佬可以另找教程),每周更新1-3讲。 +我最近在重新学Solidity,巩固一下细节,也写一个“WTF Solidity极简入门”,供小白们使用(编程大佬可以另找教程),每周更新1-3讲。 推特:[@0xAA_Science](https://twitter.com/0xAA_Science) @@ -22,9 +22,11 @@ tags: 在以太坊链上,用户(外部账户,`EOA`)可以创建智能合约,智能合约同样也可以创建新的智能合约。去中心化交易所`uniswap`就是利用工厂合约(`PairFactory`)创建了无数个币对合约(`Pair`)。这一讲,我会用简化版的`uniswap`讲如何通过合约创建合约。 ## `create` + 有两种方法可以在合约中创建新合约,`create`和`create2`,这里我们讲`create`,下一讲会介绍`create2`。 `create`的用法很简单,就是`new`一个合约,并传入新合约构造函数所需的参数: + ```solidity Contract x = new Contract{value: _value}(params) ``` @@ -32,6 +34,7 @@ Contract x = new Contract{value: _value}(params) 其中`Contract`是要创建的合约名,`x`是合约对象(地址),如果构造函数是`payable`,可以创建时转入`_value`数量的`ETH`,`params`是新合约构造函数的参数。 ## 极简Uniswap + `Uniswap V2`[核心合约](https://github.com/Uniswap/v2-core/tree/master/contracts)中包含两个合约: 1. UniswapV2Pair: 币对合约,用于管理币对地址、流动性、买卖。 @@ -39,7 +42,7 @@ Contract x = new Contract{value: _value}(params) 下面我们用`create`方法实现一个极简版的`Uniswap`:`Pair`币对合约负责管理币对地址,`PairFactory`工厂合约用于创建新的币对,并管理币对地址。 -### `Pair`合约 +### `Pair`合约 ```solidity contract Pair{ @@ -59,6 +62,7 @@ contract Pair{ } } ``` + `Pair`合约很简单,包含3个状态变量:`factory`,`token0`和`token1`。 构造函数`constructor`在部署时将`factory`赋值为工厂合约地址。`initialize`函数会由工厂合约在部署完成后手动调用以初始化代币地址,将`token0`和`token1`更新为币对中两种代币的地址。 @@ -68,6 +72,7 @@ contract Pair{ > **答**:因为`uniswap`使用的是`create2`创建合约,生成的合约地址可以实现预测,更多详情请阅读[第25讲](https://github.com/AmazingAng/WTF-Solidity/blob/main/25_Create2/readme.md)。 ### `PairFactory` + ```solidity contract PairFactory{ mapping(address => mapping(address => address)) public getPair; // 通过两个代币地址查Pair地址 @@ -86,32 +91,34 @@ contract PairFactory{ } } ``` + 工厂合约(`PairFactory`)有两个状态变量`getPair`是两个代币地址到币对地址的`map`,方便根据代币找到币对地址;`allPairs`是币对地址的数组,存储了所有代币地址。 `PairFactory`合约只有一个`createPair`函数,根据输入的两个代币地址`tokenA`和`tokenB`来创建新的`Pair`合约。其中 + ```solidity - Pair pair = new Pair(); +Pair pair = new Pair(); ``` + 就是创建合约的代码,非常简单。大家可以部署好`PairFactory`合约,然后用下面两个地址作为参数调用`createPair`,看看创建的币对地址是什么: -``` + +```text WBNB地址: 0x2c44b726ADF1963cA47Af88B284C06f30380fC78 -BSC链上的PEOPLE地址: -0xbb4CdB9CBd36B01bD1cBaEBF2De08d9173bc095c +BSC链上的PEOPLE地址: 0xbb4CdB9CBd36B01bD1cBaEBF2De08d9173bc095c ``` ### 在remix上验证 -1.使用`WBNB`和`PEOPLE`的地址作为参数调用`createPair`,得到`Pair`合约地址:0xD3e2008b4Da2cD6DEAF73471590fF30C86778A48 +1. 使用`WBNB`和`PEOPLE`的地址作为参数调用`createPair`,得到`Pair`合约地址:0xD3e2008b4Da2cD6DEAF73471590fF30C86778A48 -![](./img/24-1.png) + ![24-1](./img/24-1.png) +2. 查看`Pair`合约变量 -2.查看`Pair`合约变量 + ![24-2](./img/24-2.png) +3. Debug查看`create`操作码 -![](./img/24-2.png) - -3.Debug查看`create`操作码 - -![](./img/24-3.png) + ![24-3](./img/24-3.png) ## 总结 + 这一讲,我们用极简`Uniswap`的例子介绍了如何使用`create`方法再合约里创建合约,下一讲我们将介绍如何使用`create2`方法来实现极简`Uniswap`。 diff --git a/25_Create2/Create2.sol b/25_Create2/Create2.sol index 47a8dc6f5..d7482cb9d 100644 --- a/25_Create2/Create2.sol +++ b/25_Create2/Create2.sol @@ -19,37 +19,37 @@ contract Pair{ } contract PairFactory2{ - mapping(address => mapping(address => address)) public getPair; // 通过两个代币地址查Pair地址 - address[] public allPairs; // 保存所有Pair地址 + mapping(address => mapping(address => address)) public getPair; // 通过两个代币地址查Pair地址 + address[] public allPairs; // 保存所有Pair地址 - function createPair2(address tokenA, address tokenB) external returns (address pairAddr) { - require(tokenA != tokenB, 'IDENTICAL_ADDRESSES'); //避免tokenA和tokenB相同产生的冲突 - // 计算用tokenA和tokenB地址计算salt - (address token0, address token1) = tokenA < tokenB ? (tokenA, tokenB) : (tokenB, tokenA); //将tokenA和tokenB按大小排序 - bytes32 salt = keccak256(abi.encodePacked(token0, token1)); - // 用create2部署新合约 - Pair pair = new Pair{salt: salt}(); - // 调用新合约的initialize方法 - pair.initialize(tokenA, tokenB); - // 更新地址map - pairAddr = address(pair); - allPairs.push(pairAddr); - getPair[tokenA][tokenB] = pairAddr; - getPair[tokenB][tokenA] = pairAddr; - } + function createPair2(address tokenA, address tokenB) external returns (address pairAddr) { + require(tokenA != tokenB, 'IDENTICAL_ADDRESSES'); //避免tokenA和tokenB相同产生的冲突 + // 计算用tokenA和tokenB地址计算salt + (address token0, address token1) = tokenA < tokenB ? (tokenA, tokenB) : (tokenB, tokenA); //将tokenA和tokenB按大小排序 + bytes32 salt = keccak256(abi.encodePacked(token0, token1)); + // 用create2部署新合约 + Pair pair = new Pair{salt: salt}(); + // 调用新合约的initialize方法 + pair.initialize(tokenA, tokenB); + // 更新地址map + pairAddr = address(pair); + allPairs.push(pairAddr); + getPair[tokenA][tokenB] = pairAddr; + getPair[tokenB][tokenA] = pairAddr; + } - // 提前计算pair合约地址 - function calculateAddr(address tokenA, address tokenB) public view returns(address predictedAddress){ - require(tokenA != tokenB, 'IDENTICAL_ADDRESSES'); //避免tokenA和tokenB相同产生的冲突 - // 计算用tokenA和tokenB地址计算salt - (address token0, address token1) = tokenA < tokenB ? (tokenA, tokenB) : (tokenB, tokenA); //将tokenA和tokenB按大小排序 - bytes32 salt = keccak256(abi.encodePacked(token0, token1)); - // 计算合约地址方法 hash() - predictedAddress = address(uint160(uint(keccak256(abi.encodePacked( - bytes1(0xff), - address(this), - salt, - keccak256(type(Pair).creationCode) - ))))); - } + // 提前计算pair合约地址 + function calculateAddr(address tokenA, address tokenB) public view returns(address predictedAddress){ + require(tokenA != tokenB, 'IDENTICAL_ADDRESSES'); //避免tokenA和tokenB相同产生的冲突 + // 计算用tokenA和tokenB地址计算salt + (address token0, address token1) = tokenA < tokenB ? (tokenA, tokenB) : (tokenB, tokenA); //将tokenA和tokenB按大小排序 + bytes32 salt = keccak256(abi.encodePacked(token0, token1)); + // 计算合约地址方法 hash() + predictedAddress = address(uint160(uint(keccak256(abi.encodePacked( + bytes1(0xff), + address(this), + salt, + keccak256(type(Pair).creationCode) + ))))); + } } diff --git a/25_Create2/readme.md b/25_Create2/readme.md index a2237d29e..cc10de201 100644 --- a/25_Create2/readme.md +++ b/25_Create2/readme.md @@ -8,51 +8,63 @@ tags: - create2 --- -# WTF Solidity极简入门: 25. Create2 +# WTF Solidity极简入门: 25. CREATE2 -我最近在重新学solidity,巩固一下细节,也写一个“WTF Solidity极简入门”,供小白们使用(编程大佬可以另找教程),每周更新1-3讲。 +我最近在重新学Solidity,巩固一下细节,也写一个“WTF Solidity极简入门”,供小白们使用(编程大佬可以另找教程),每周更新1-3讲。 -欢迎关注我的推特:[@0xAA_Science](https://twitter.com/0xAA_Science) +推特:[@0xAA_Science](https://twitter.com/0xAA_Science) -欢迎加入WTF科学家社群,内有加微信群方法:[链接](https://discord.gg/5akcruXrsk) +社区:[Discord](https://discord.gg/5akcruXrsk)|[微信群](https://docs.google.com/forms/d/e/1FAIpQLSe4KGT8Sh6sJ7hedQRuIYirOoZK_85miz3dw7vA1-YjodgJ-A/viewform?usp=sf_link)|[官网 wtf.academy](https://wtf.academy) -所有代码和教程开源在github(1024个star发课程认证,2048个star发社群NFT): [github.com/AmazingAng/WTFSolidity](https://github.com/AmazingAng/WTFSolidity) +所有代码和教程开源在github: [github.com/AmazingAng/WTFSolidity](https://github.com/AmazingAng/WTFSolidity) ----- ## CREATE2 + `CREATE2` 操作码使我们在智能合约部署在以太坊网络之前就能预测合约的地址。`Uniswap`创建`Pair`合约用的就是`CREATE2`而不是`CREATE`。这一讲,我将介绍`CREATE2`的用法 ### CREATE如何计算地址 -智能合约可以由其他合约和普通账户利用`CREATE`操作码创建。 在这两种情况下,新合约的地址都以相同的方式计算:创建者的地址(通常为部署的钱包地址或者合约地址)和`nonce`(该地址发送交易的总数,对于合约账户是创建的合约总数,每创建一个合约nonce+1))的哈希。 -``` + +智能合约可以由其他合约和普通账户利用`CREATE`操作码创建。 在这两种情况下,新合约的地址都以相同的方式计算:创建者的地址(通常为部署的钱包地址或者合约地址)和`nonce`(该地址发送交易的总数,对于合约账户是创建的合约总数,每创建一个合约nonce+1)的哈希。 + +```text 新地址 = hash(创建者地址, nonce) ``` + 创建者地址不会变,但`nonce`可能会随时间而改变,因此用`CREATE`创建的合约地址不好预测。 ### CREATE2如何计算地址 + `CREATE2`的目的是为了让合约地址独立于未来的事件。不管未来区块链上发生了什么,你都可以把合约部署在事先计算好的地址上。用`CREATE2`创建的合约地址由4个部分决定: + - `0xFF`:一个常数,避免和`CREATE`冲突 -- `CreatorAddress`: 调用 Create2 的当前合约(创建合约)地址。 -- `salt`(盐):一个创建者指定的 uint256 类型的值,的主要目的是用来影响新创建的合约的地址。 +- `CreatorAddress`: 调用 CREATE2 的当前合约(创建合约)地址。 +- `salt`(盐):一个创建者指定的`bytes32`类型的值,它的主要目的是用来影响新创建的合约的地址。 - `initcode`: 新合约的初始字节码(合约的Creation Code和构造函数的参数)。 -``` + +```text 新地址 = hash("0xFF",创建者地址, salt, initcode) ``` + `CREATE2` 确保,如果创建者使用 `CREATE2` 和提供的 `salt` 部署给定的合约`initcode`,它将存储在 `新地址` 中。 ## 如何使用`CREATE2` -`CREATE2`的用法和之前讲的`Create`类似,同样是`new`一个合约,并传入新合约构造函数所需的参数,只不过要多传一个`salt`参数: -``` + +`CREATE2`的用法和之前讲的`CREATE`类似,同样是`new`一个合约,并传入新合约构造函数所需的参数,只不过要多传一个`salt`参数: + +```solidity Contract x = new Contract{salt: _salt, value: _value}(params) ``` + 其中`Contract`是要创建的合约名,`x`是合约对象(地址),`_salt`是指定的盐;如果构造函数是`payable`,可以创建时转入`_value`数量的`ETH`,`params`是新合约构造函数的参数。 ## 极简Uniswap2 -跟[上一讲](https://mirror.xyz/wtfacademy.eth/kojopp2CgDK3ehHxXc_2fkZe87uM0O5OmsEU6y83eJs)类似,我们用`Create2`来实现极简`Uniswap`。 +跟[上一讲](https://mirror.xyz/wtfacademy.eth/kojopp2CgDK3ehHxXc_2fkZe87uM0O5OmsEU6y83eJs)类似,我们用`CREATE2`来实现极简`Uniswap`。 ### `Pair` + ```solidity contract Pair{ address public factory; // 工厂合约地址 @@ -71,78 +83,87 @@ contract Pair{ } } ``` + `Pair`合约很简单,包含3个状态变量:`factory`,`token0`和`token1`。 构造函数`constructor`在部署时将`factory`赋值为工厂合约地址。`initialize`函数会在`Pair`合约创建的时候被工厂合约调用一次,将`token0`和`token1`更新为币对中两种代币的地址。 ### `PairFactory2` + ```solidity contract PairFactory2{ - mapping(address => mapping(address => address)) public getPair; // 通过两个代币地址查Pair地址 - address[] public allPairs; // 保存所有Pair地址 - - function createPair2(address tokenA, address tokenB) external returns (address pairAddr) { - require(tokenA != tokenB, 'IDENTICAL_ADDRESSES'); //避免tokenA和tokenB相同产生的冲突 - // 用tokenA和tokenB地址计算salt - (address token0, address token1) = tokenA < tokenB ? (tokenA, tokenB) : (tokenB, tokenA); //将tokenA和tokenB按大小排序 - bytes32 salt = keccak256(abi.encodePacked(token0, token1)); - // 用create2部署新合约 - Pair pair = new Pair{salt: salt}(); - // 调用新合约的initialize方法 - pair.initialize(tokenA, tokenB); - // 更新地址map - pairAddr = address(pair); - allPairs.push(pairAddr); - getPair[tokenA][tokenB] = pairAddr; - getPair[tokenB][tokenA] = pairAddr; - } + mapping(address => mapping(address => address)) public getPair; // 通过两个代币地址查Pair地址 + address[] public allPairs; // 保存所有Pair地址 + + function createPair2(address tokenA, address tokenB) external returns (address pairAddr) { + require(tokenA != tokenB, 'IDENTICAL_ADDRESSES'); //避免tokenA和tokenB相同产生的冲突 + // 用tokenA和tokenB地址计算salt + (address token0, address token1) = tokenA < tokenB ? (tokenA, tokenB) : (tokenB, tokenA); //将tokenA和tokenB按大小排序 + bytes32 salt = keccak256(abi.encodePacked(token0, token1)); + // 用create2部署新合约 + Pair pair = new Pair{salt: salt}(); + // 调用新合约的initialize方法 + pair.initialize(tokenA, tokenB); + // 更新地址map + pairAddr = address(pair); + allPairs.push(pairAddr); + getPair[tokenA][tokenB] = pairAddr; + getPair[tokenB][tokenA] = pairAddr; + } +} ``` + 工厂合约(`PairFactory2`)有两个状态变量`getPair`是两个代币地址到币对地址的`map`,方便根据代币找到币对地址;`allPairs`是币对地址的数组,存储了所有币对地址。 `PairFactory2`合约只有一个`createPair2`函数,使用`CREATE2`根据输入的两个代币地址`tokenA`和`tokenB`来创建新的`Pair`合约。其中 + ```solidity - Pair pair = new Pair{salt: salt}(); +Pair pair = new Pair{salt: salt}(); ``` + 就是利用`CREATE2`创建合约的代码,非常简单,而`salt`为`token1`和`token2`的`hash`: + ```solidity - bytes32 salt = keccak256(abi.encodePacked(token0, token1)); +bytes32 salt = keccak256(abi.encodePacked(token0, token1)); ``` ### 事先计算`Pair`地址 + ```solidity - // 提前计算pair合约地址 - function calculateAddr(address tokenA, address tokenB) public view returns(address predictedAddress){ - require(tokenA != tokenB, 'IDENTICAL_ADDRESSES'); //避免tokenA和tokenB相同产生的冲突 - // 计算用tokenA和tokenB地址计算salt - (address token0, address token1) = tokenA < tokenB ? (tokenA, tokenB) : (tokenB, tokenA); //将tokenA和tokenB按大小排序 - bytes32 salt = keccak256(abi.encodePacked(token0, token1)); - // 计算合约地址方法 hash() - predictedAddress = address(uint160(uint(keccak256(abi.encodePacked( - bytes1(0xff), - address(this), - salt, - keccak256(type(Pair).creationCode) - ))))); - } +// 提前计算pair合约地址 +function calculateAddr(address tokenA, address tokenB) public view returns(address predictedAddress){ + require(tokenA != tokenB, 'IDENTICAL_ADDRESSES'); //避免tokenA和tokenB相同产生的冲突 + // 计算用tokenA和tokenB地址计算salt + (address token0, address token1) = tokenA < tokenB ? (tokenA, tokenB) : (tokenB, tokenA); //将tokenA和tokenB按大小排序 + bytes32 salt = keccak256(abi.encodePacked(token0, token1)); + // 计算合约地址方法 hash() + predictedAddress = address(uint160(uint(keccak256(abi.encodePacked( + bytes1(0xff), + address(this), + salt, + keccak256(type(Pair).creationCode) + ))))); +} ``` + 我们写了一个`calculateAddr`函数来事先计算`tokenA`和`tokenB`将会生成的`Pair`地址。通过它,我们可以验证我们事先计算的地址和实际地址是否相同。 大家可以部署好`PairFactory2`合约,然后用下面两个地址作为参数调用`createPair2`,看看创建的币对地址是什么,是否与事先计算的地址一样: -``` + +```text WBNB地址: 0x2c44b726ADF1963cA47Af88B284C06f30380fC78 -BSC链上的PEOPLE地址: -0xbb4CdB9CBd36B01bD1cBaEBF2De08d9173bc095c +BSC链上的PEOPLE地址: 0xbb4CdB9CBd36B01bD1cBaEBF2De08d9173bc095c ``` #### 如果部署合约构造函数中存在参数 - 例如当create2合约时: -> Pair pair = new Pair{salt: salt}(address(this)); +> Pair pair = new Pair{salt: salt}(address(this)); 计算时,需要将参数和initcode一起进行打包: > ~~keccak256(type(Pair).creationCode)~~ > => keccak256(abi.encodePacked(type(Pair).creationCode, abi.encode(address(this)))) + ```solidity predictedAddress = address(uint160(uint(keccak256(abi.encodePacked( bytes1(0xff), @@ -153,20 +174,19 @@ predictedAddress = address(uint160(uint(keccak256(abi.encodePacked( ``` ### 在remix上验证 + 1. 首先用`WBNB`和`PEOPLE`的地址哈希作为`salt`来计算出`Pair`合约的地址 2. 调用`PairFactory2.createPair2`传入参数为`WBNB`和`PEOPLE`的地址,获取出创建的`pair`合约地址 3. 对比合约地址 -![create2_remix_test.png](./img/25-1.png) - + ![create2_remix_test.png](./img/25-1.png) ## create2的实际应用场景 -1. 交易所为新用户预留创建钱包合约地址。 -2. 由 `CREATE2` 驱动的 `factory` 合约,在`uniswapV2`中交易对的创建是在 `Factory`中调用`create2`完成。这样做的好处是: 它可以得到一个确定的`pair`地址, 使得 `Router`中就可以通过 `(tokenA, tokenB)` 计算出`pair`地址, 不再需要执行一次 `Factory.getPair(tokenA, tokenB)` 的跨合约调用。 +1. 交易所为新用户预留创建钱包合约地址。 +2. 由 `CREATE2` 驱动的 `factory` 合约,在`Uniswap V2`中交易对的创建是在 `Factory`中调用`CREATE2`完成。这样做的好处是: 它可以得到一个确定的`pair`地址, 使得 `Router`中就可以通过 `(tokenA, tokenB)` 计算出`pair`地址, 不再需要执行一次 `Factory.getPair(tokenA, tokenB)` 的跨合约调用。 ## 总结 -这一讲,我们介绍了`CREATE2`操作码的原理,使用方法,并用它完成了极简版的`Uniswap`并提前计算币对合约地址。`CREATE2`让我们可以在部署合约前确定它的合约地址,这也是 -一些`layer2`项目的基础。 +这一讲,我们介绍了`CREATE2`操作码的原理,使用方法,并用它完成了极简版的`Uniswap`并提前计算币对合约地址。`CREATE2`让我们可以在部署合约前确定它的合约地址,这也是一些`layer2`项目的基础。 diff --git a/26_DeleteContract/img/26-1.png b/26_DeleteContract/img/26-1.png old mode 100644 new mode 100755 index b2ad5a546..5a08bb591 Binary files a/26_DeleteContract/img/26-1.png and b/26_DeleteContract/img/26-1.png differ diff --git a/26_DeleteContract/img/26-2.png b/26_DeleteContract/img/26-2.png old mode 100644 new mode 100755 index 8ddd54d87..3d572bdc4 Binary files a/26_DeleteContract/img/26-2.png and b/26_DeleteContract/img/26-2.png differ diff --git a/26_DeleteContract/readme.md b/26_DeleteContract/readme.md index 6f92f3294..2b44b5541 100644 --- a/26_DeleteContract/readme.md +++ b/26_DeleteContract/readme.md @@ -10,13 +10,13 @@ tags: # WTF Solidity极简入门: 26. 删除合约 -我最近在重新学solidity,巩固一下细节,也写一个“WTF Solidity极简入门”,供小白们使用(编程大佬可以另找教程),每周更新1-3讲。 +我最近在重新学Solidity,巩固一下细节,也写一个“WTF Solidity极简入门”,供小白们使用(编程大佬可以另找教程),每周更新1-3讲。 -欢迎关注我的推特:[@0xAA_Science](https://twitter.com/0xAA_Science) +推特:[@0xAA_Science](https://twitter.com/0xAA_Science) -欢迎加入WTF科学家社区,内有加微信群方法:[链接](https://discord.gg/5akcruXrsk) +社区:[Discord](https://discord.gg/5akcruXrsk)|[微信群](https://docs.google.com/forms/d/e/1FAIpQLSe4KGT8Sh6sJ7hedQRuIYirOoZK_85miz3dw7vA1-YjodgJ-A/viewform?usp=sf_link)|[官网 wtf.academy](https://wtf.academy) -所有代码和教程开源在github(1024个star发课程认证,2048个star发社群NFT): [github.com/AmazingAng/WTFSolidity](https://github.com/AmazingAng/WTFSolidity) +所有代码和教程开源在github: [github.com/AmazingAng/WTFSolidity](https://github.com/AmazingAng/WTFSolidity) ----- @@ -25,14 +25,17 @@ tags: `selfdestruct`命令可以用来删除智能合约,并将该合约剩余`ETH`转到指定地址。`selfdestruct`是为了应对合约出错的极端情况而设计的。它最早被命名为`suicide`(自杀),但是这个词太敏感。为了保护抑郁的程序员,改名为`selfdestruct`;在 [v0.8.18](https://blog.soliditylang.org/2023/02/01/solidity-0.8.18-release-announcement/) 版本中,`selfdestruct` 关键字被标记为「不再建议使用」,在一些情况下它会导致预期之外的合约语义,但由于目前还没有代替方案,目前只是对开发者做了编译阶段的警告,相关内容可以查看 [EIP-6049](https://eips.ethereum.org/EIPS/eip-6049)。 ### 如何使用`selfdestruct` + `selfdestruct`使用起来非常简单: + ```solidity selfdestruct(_addr); ``` -其中`_addr`是接收合约中剩余`ETH`的地址。 -`_addr` 地址不需要有`receive()`或`fallback()`也能接收`ETH`。 + +其中`_addr`是接收合约中剩余`ETH`的地址。`_addr` 地址不需要有`receive()`或`fallback()`也能接收`ETH`。 ### 例子 + ```solidity contract DeleteContract { @@ -52,30 +55,28 @@ contract DeleteContract { } } ``` + 在`DeleteContract`合约中,我们写了一个`public`状态变量`value`,两个函数:`getBalance()`用于获取合约`ETH`余额,`deleteContract()`用于自毁合约,并把`ETH`转入给发起人。 部署好合约后,我们向`DeleteContract`合约转入1 `ETH`。这时,`getBalance()`会返回1 `ETH`,`value`变量是10。 -当我们调用`deleteContract()`函数,合约将自毁,所有变量都清空,此时`value`变为默认值`0`,`getBalance()`也返回空值。 +当我们调用`deleteContract()`函数,合约将自毁,此时再次调用合约函数交互会失败。 ### 注意事项 -1. 对外提供合约销毁接口时,最好设置为只有合约所有者可以调用,可以使用函数修饰符`onlyOwner`进行函数声明。 -2. 当合约被销毁后与智能合约的交互也能成功,并且返回0。 +1. 对外提供合约销毁接口时,最好设置为只有合约所有者可以调用,可以使用函数修饰符`onlyOwner`进行函数声明。 +2. 当合约被销毁后再次与合约函数交互会报error。 +3. 当合约中有`selfdestruct`功能时常常会带来安全问题和信任问题,合约中的selfdestruct功能会为攻击者打开攻击向量(例如使用`selfdestruct`向一个合约频繁转入token进行攻击,这将大大节省了GAS的费用,虽然很少人这么做),此外,此功能还会降低用户对合约的信心。 -3. 当合约中有`selfdestruct`功能时常常会带来安全问题和信任问题,合约中的Selfdestruct功能会为攻击者打开攻击向量(例如使用`selfdestruct`向一个合约频繁转入token进行攻击,这将大大节省了GAS的费用,虽然很少人这么做),此外,此功能还会降低用户对合约的信心。 +### 在remix上验证 -### 在remix上验证 1. 部署合约并且转入1ETH,查看合约状态 -![deployContract.png](./img/26-2.png) - + ![deployContract.png](./img/26-1.png) 2. 销毁合约,查看合约状态 -![deleteContract.png](./img/26-1.png) - -从测试中观察合约状态可以发现合约销毁后的ETH返回给了指定的地址,并且在合约销毁后依然可以请求交互,所以我们不能根据这个来判断合约是否已经销毁。 - + ![deleteContract.png](./img/26-2.png) +从测试中观察合约状态可以发现合约销毁后的ETH返回给了指定的地址,在合约销毁后再次调用合约函数进行交互则会失败。 ## 总结 diff --git a/27_ABIEncode/readme.md b/27_ABIEncode/readme.md index b1d76025a..6203e0641 100644 --- a/27_ABIEncode/readme.md +++ b/27_ABIEncode/readme.md @@ -10,13 +10,13 @@ tags: # WTF Solidity极简入门: 27. ABI编码解码 -我最近在重新学solidity,巩固一下细节,也写一个“WTF Solidity极简入门”,供小白们使用(编程大佬可以另找教程),每周更新1-3讲。 +我最近在重新学Solidity,巩固一下细节,也写一个“WTF Solidity极简入门”,供小白们使用(编程大佬可以另找教程),每周更新1-3讲。 -欢迎关注我的推特:[@0xAA_Science](https://twitter.com/0xAA_Science) +推特:[@0xAA_Science](https://twitter.com/0xAA_Science) -欢迎加入WTF科学家社区,内有加微信群方法:[链接](https://discord.gg/5akcruXrsk) +社区:[Discord](https://discord.gg/5akcruXrsk)|[微信群](https://docs.google.com/forms/d/e/1FAIpQLSe4KGT8Sh6sJ7hedQRuIYirOoZK_85miz3dw7vA1-YjodgJ-A/viewform?usp=sf_link)|[官网 wtf.academy](https://wtf.academy) -所有代码和教程开源在github(1024个star发课程认证,2048个star发社群NFT): [github.com/AmazingAng/WTFSolidity](https://github.com/AmazingAng/WTFSolidity) +所有代码和教程开源在github: [github.com/AmazingAng/WTFSolidity](https://github.com/AmazingAng/WTFSolidity) ----- @@ -27,79 +27,96 @@ tags: ## ABI编码 我们将编码4个变量,他们的类型分别是`uint256`(别名 uint), `address`, `string`, `uint256[2]`: + ```solidity - uint x = 10; - address addr = 0x7A58c0Be72BE218B41C608b7Fe7C5bB630736C71; - string name = "0xAA"; - uint[2] array = [5, 6]; +uint x = 10; +address addr = 0x7A58c0Be72BE218B41C608b7Fe7C5bB630736C71; +string name = "0xAA"; +uint[2] array = [5, 6]; ``` ### `abi.encode` + 将给定参数利用[ABI规则](https://learnblockchain.cn/docs/solidity/abi-spec.html)编码。`ABI`被设计出来跟智能合约交互,他将每个参数填充为32字节的数据,并拼接在一起。如果你要和合约交互,你要用的就是`abi.encode`。 + ```solidity - function encode() public view returns(bytes memory result) { - result = abi.encode(x, addr, name, array); - } +function encode() public view returns(bytes memory result) { + result = abi.encode(x, addr, name, array); +} ``` + 编码的结果为`0x000000000000000000000000000000000000000000000000000000000000000a0000000000000000000000007a58c0be72be218b41c608b7fe7c5bb630736c7100000000000000000000000000000000000000000000000000000000000000a00000000000000000000000000000000000000000000000000000000000000005000000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000043078414100000000000000000000000000000000000000000000000000000000`,由于`abi.encode`将每个数据都填充为32字节,中间有很多`0`。 ### `abi.encodePacked` + 将给定参数根据其所需最低空间编码。它类似 `abi.encode`,但是会把其中填充的很多`0`省略。比如,只用1字节来编码`uint8`类型。当你想省空间,并且不与合约交互的时候,可以使用`abi.encodePacked`,例如算一些数据的`hash`时。 ```solidity - function encodePacked() public view returns(bytes memory result) { - result = abi.encodePacked(x, addr, name, array); - } +function encodePacked() public view returns(bytes memory result) { + result = abi.encodePacked(x, addr, name, array); +} ``` + 编码的结果为`0x000000000000000000000000000000000000000000000000000000000000000a7a58c0be72be218b41c608b7fe7c5bb630736c713078414100000000000000000000000000000000000000000000000000000000000000050000000000000000000000000000000000000000000000000000000000000006`,由于`abi.encodePacked`对编码进行了压缩,长度比`abi.encode`短很多。 ### `abi.encodeWithSignature` + 与`abi.encode`功能类似,只不过第一个参数为`函数签名`,比如`"foo(uint256,address,string,uint256[2])"`。当调用其他合约的时候可以使用。 + ```solidity - function encodeWithSignature() public view returns(bytes memory result) { - result = abi.encodeWithSignature("foo(uint256,address,string,uint256[2])", x, addr, name, array); - } +function encodeWithSignature() public view returns(bytes memory result) { + result = abi.encodeWithSignature("foo(uint256,address,string,uint256[2])", x, addr, name, array); +} ``` + 编码的结果为`0xe87082f1000000000000000000000000000000000000000000000000000000000000000a0000000000000000000000007a58c0be72be218b41c608b7fe7c5bb630736c7100000000000000000000000000000000000000000000000000000000000000a00000000000000000000000000000000000000000000000000000000000000005000000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000043078414100000000000000000000000000000000000000000000000000000000`,等同于在`abi.encode`编码结果前加上了4字节的`函数选择器`[^说明]。 [^说明]: 函数选择器就是通过函数名和参数进行签名处理(Keccak–Sha3)来标识函数,可以用于不同合约之间的函数调用 ### `abi.encodeWithSelector` + 与`abi.encodeWithSignature`功能类似,只不过第一个参数为`函数选择器`,为`函数签名`Keccak哈希的前4个字节。 ```solidity - function encodeWithSelector() public view returns(bytes memory result) { - result = abi.encodeWithSelector(bytes4(keccak256("foo(uint256,address,string,uint256[2])")), x, addr, name, array); - } +function encodeWithSelector() public view returns(bytes memory result) { + result = abi.encodeWithSelector(bytes4(keccak256("foo(uint256,address,string,uint256[2])")), x, addr, name, array); +} ``` 编码的结果为`0xe87082f1000000000000000000000000000000000000000000000000000000000000000a0000000000000000000000007a58c0be72be218b41c608b7fe7c5bb630736c7100000000000000000000000000000000000000000000000000000000000000a00000000000000000000000000000000000000000000000000000000000000005000000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000043078414100000000000000000000000000000000000000000000000000000000`,与`abi.encodeWithSignature`结果一样。 ## ABI解码 + ### `abi.decode` + `abi.decode`用于解码`abi.encode`生成的二进制编码,将它还原成原本的参数。 ```solidity - function decode(bytes memory data) public pure returns(uint dx, address daddr, string memory dname, uint[2] memory darray) { - (dx, daddr, dname, darray) = abi.decode(data, (uint, address, string, uint[2])); - } +function decode(bytes memory data) public pure returns(uint dx, address daddr, string memory dname, uint[2] memory darray) { + (dx, daddr, dname, darray) = abi.decode(data, (uint, address, string, uint[2])); +} ``` + 我们将`abi.encode`的二进制编码输入给`decode`,将解码出原来的参数: -![](https://images.mirror-media.xyz/publication-images/jboRaaq0U57qVYjmsOgbv.png?height=408&width=624) +![27-3](https://images.mirror-media.xyz/publication-images/jboRaaq0U57qVYjmsOgbv.png?height=408&width=624) ## 在remix上验证 + - 部署合约查看abi.encode方法的编码结果 -![](./img/27-1.png) + ![27-1](./img/27-1.png) - 对比验证四种编码方法的异同点 -![](./img/27-2.png) + ![27-2](./img/27-2.png) - 查看abi.decode方法的解码结果 -![](./img/27-3.png) + + ![27-3](./img/27-3.png) ## ABI的使用场景 + 1. 在合约开发中,ABI常配合call来实现对合约的底层调用。 -```solidity + + ```solidity bytes4 selector = contract.getValue.selector; bytes memory data = abi.encodeWithSelector(selector, _x); @@ -107,31 +124,38 @@ tags: require(success); return abi.decode(returnedData, (uint256)); -``` + ``` + 2. ethers.js中常用ABI实现合约的导入和函数调用。 -```solidity + + ```solidity const wavePortalContract = new ethers.Contract(contractAddress, contractABI, signer); /* * Call the getAllWaves method from your Smart Contract */ const waves = await wavePortalContract.getAllWaves(); -``` + ``` + 3. 对不开源合约进行反编译后,某些函数无法查到函数签名,可通过ABI进行调用。 -- 0x533ba33a() 是一个反编译后显示的函数,只有函数编码后的结果,并且无法查到函数签名 -![](./img/27-4.png) -![](./img/27-5.png) -- 这种情况无法通过构造interface接口或contract来进行调用 -![](./img/27-6.png) + - 0x533ba33a() 是一个反编译后显示的函数,只有函数编码后的结果,并且无法查到函数签名 -这种情况下,就可以通过ABI函数选择器来调用 -```solidity + ![27-4](./img/27-4.png) + ![27-5](./img/27-5.png) + + - 这种情况无法通过构造interface接口或contract来进行调用 + ![27-6](./img/27-6.png) + + 这种情况下,就可以通过ABI函数选择器来调用 + + ```solidity bytes memory data = abi.encodeWithSelector(bytes4(0x533ba33a)); (bool success, bytes memory returnedData) = address(contract).staticcall(data); require(success); return abi.decode(returnedData, (uint256)); -``` + ``` ## 总结 + 在以太坊中,数据必须编码成字节码才能和智能合约交互。这一讲,我们介绍了4种`abi编码`方法和1种`abi解码`方法。 diff --git a/28_Hash/readme.md b/28_Hash/readme.md index 8a3e2c5f4..446d3a111 100644 --- a/28_Hash/readme.md +++ b/28_Hash/readme.md @@ -9,96 +9,110 @@ tags: # WTF Solidity极简入门: 28. Hash -我最近在重新学solidity,巩固一下细节,也写一个“WTF Solidity极简入门”,供小白们使用(编程大佬可以另找教程),每周更新1-3讲。 +我最近在重新学Solidity,巩固一下细节,也写一个“WTF Solidity极简入门”,供小白们使用(编程大佬可以另找教程),每周更新1-3讲。 -欢迎关注我的推特:[@0xAA_Science](https://twitter.com/0xAA_Science) +推特:[@0xAA_Science](https://twitter.com/0xAA_Science) -欢迎加入WTF科学家社区,内有加微信群方法:[链接](https://discord.gg/5akcruXrsk) +社区:[Discord](https://discord.gg/5akcruXrsk)|[微信群](https://docs.google.com/forms/d/e/1FAIpQLSe4KGT8Sh6sJ7hedQRuIYirOoZK_85miz3dw7vA1-YjodgJ-A/viewform?usp=sf_link)|[官网 wtf.academy](https://wtf.academy) -所有代码和教程开源在github(1024个star发课程认证,2048个star发社群NFT): [github.com/AmazingAng/WTFSolidity](https://github.com/AmazingAng/WTFSolidity) +所有代码和教程开源在github: [github.com/AmazingAng/WTFSolidity](https://github.com/AmazingAng/WTFSolidity) ----- -哈希函数(hash function)是一个密码学概念,它可以将任意长度的消息转换为一个固定长度的值,这个值也称作哈希(hash)。这一讲,我们简单介绍一下哈希函数及在`solidity`的应用 +哈希函数(hash function)是一个密码学概念,它可以将任意长度的消息转换为一个固定长度的值,这个值也称作哈希(hash)。这一讲,我们简单介绍一下哈希函数及在`Solidity`的应用。 ## Hash的性质 + 一个好的哈希函数应该具有以下几个特性: + - 单向性:从输入的消息到它的哈希的正向运算简单且唯一确定,而反过来非常难,只能靠暴力枚举。 - 灵敏性:输入的消息改变一点对它的哈希改变很大。 - 高效性:从输入的消息到哈希的运算高效。 - 均一性:每个哈希值被取到的概率应该基本相等。 - 抗碰撞性: - - 弱抗碰撞性:给定一个消息`x`,找到另一个消息`x'`使得`hash(x) = hash(x')`是困难的。 - - 强抗碰撞性:找到任意`x`和`x'`,使得`hash(x) = hash(x')`是困难的。 + - 弱抗碰撞性:给定一个消息`x`,找到另一个消息`x'`,使得`hash(x) = hash(x')`是困难的。 + - 强抗碰撞性:找到任意`x`和`x'`,使得`hash(x) = hash(x')`是困难的。 ## Hash的应用 + - 生成数据唯一标识 - 加密签名 - 安全加密 ## Keccak256 -`Keccak256`函数是`solidity`中最常用的哈希函数,用法非常简单: + +`Keccak256`函数是`Solidity`中最常用的哈希函数,用法非常简单: + ```solidity 哈希 = keccak256(数据); ``` + ### Keccak256和sha3 + 这是一个很有趣的事情: + 1. sha3由keccak标准化而来,在很多场合下Keccak和SHA3是同义词,但在2015年8月SHA3最终完成标准化时,NIST调整了填充算法。**所以SHA3就和keccak计算的结果不一样**,这点在实际开发中要注意。 2. 以太坊在开发的时候sha3还在标准化中,所以采用了keccak,所以Ethereum和Solidity智能合约代码中的SHA3是指Keccak256,而不是标准的NIST-SHA3,为了避免混淆,直接在合约代码中写成Keccak256是最清晰的。 ### 生成数据唯一标识 我们可以利用`keccak256`来生成一些数据的唯一标识。比如我们有几个不同类型的数据:`uint`,`string`,`address`,我们可以先用`abi.encodePacked`方法将他们打包编码,然后再用`keccak256`来生成唯一标识: + ```solidity - function hash( - uint _num, - string memory _string, - address _addr +function hash( + uint _num, + string memory _string, + address _addr ) public pure returns (bytes32) { - return keccak256(abi.encodePacked(_num, _string, _addr)); - } + return keccak256(abi.encodePacked(_num, _string, _addr)); +} ``` ### 弱抗碰撞性 -我们用`keccak256`演示一下之前讲到的弱抗碰撞性,即给定一个消息`x`,找到另一个消息`x'`使得`hash(x) = hash(x')`是困难的。 + +我们用`keccak256`演示一下之前讲到的弱抗碰撞性,即给定一个消息`x`,找到另一个消息`x'`,使得`hash(x) = hash(x')`是困难的。 我们给定一个消息`0xAA`,试图去找另一个消息,使得它们的哈希值相等: ```solidity - // 弱抗碰撞性 - function weak( - string memory string1 +// 弱抗碰撞性 +function weak( + string memory string1 )public view returns (bool){ - return keccak256(abi.encodePacked(string1)) == _msg; - } + return keccak256(abi.encodePacked(string1)) == _msg; +} ``` 大家可以试个10次,看看能不能幸运的碰撞上。 ### 强抗碰撞性 + 我们用`keccak256`演示一下之前讲到的强抗碰撞性,即找到任意不同的`x`和`x'`,使得`hash(x) = hash(x')`是困难的。 我们构造一个函数`strong`,接收两个不同的`string`参数`string1`和`string2`,然后判断它们的哈希是否相同: ```solidity - // 强抗碰撞性 - function strong( +// 强抗碰撞性 +function strong( string memory string1, string memory string2 )public pure returns (bool){ - return keccak256(abi.encodePacked(string1)) == keccak256(abi.encodePacked(string2)); - } + return keccak256(abi.encodePacked(string1)) == keccak256(abi.encodePacked(string2)); +} ``` 大家可以试个10次,看看能不能幸运的碰撞上。 ## 在remix上验证 + - 部署合约查看唯一标识的生成结果 -![](./img/28-1.png) + + ![28-1](./img/28-1.png) - 验证哈希函数的灵敏性,以及强、弱抗碰撞性 -![](./img/28-2.png) + + ![28-2](./img/28-2.png) ## 总结 -这一讲,我们介绍了什么是哈希函数,以及如何使用`solidity`最常用的哈希函数`keccak256`。 +这一讲,我们介绍了什么是哈希函数,以及如何使用`Solidity`最常用的哈希函数`keccak256`。 diff --git a/29_Selector/readme.md b/29_Selector/readme.md index 753d7010f..5ffc14f3f 100644 --- a/29_Selector/readme.md +++ b/29_Selector/readme.md @@ -9,18 +9,18 @@ tags: # WTF Solidity极简入门: 29. 函数选择器Selector -我最近在重新学solidity,巩固一下细节,也写一个“WTF Solidity极简入门”,供小白们使用(编程大佬可以另找教程),每周更新1-3讲。 +我最近在重新学Solidity,巩固一下细节,也写一个“WTF Solidity极简入门”,供小白们使用(编程大佬可以另找教程),每周更新1-3讲。 -欢迎关注我的推特:[@0xAA_Science](https://twitter.com/0xAA_Science) +推特:[@0xAA_Science](https://twitter.com/0xAA_Science) -欢迎加入WTF科学家社区,内有加微信群方法:[链接](https://discord.gg/5akcruXrsk) +社区:[Discord](https://discord.gg/5akcruXrsk)|[微信群](https://docs.google.com/forms/d/e/1FAIpQLSe4KGT8Sh6sJ7hedQRuIYirOoZK_85miz3dw7vA1-YjodgJ-A/viewform?usp=sf_link)|[官网 wtf.academy](https://wtf.academy) -所有代码和教程开源在github(1024个star发课程认证,2048个star发社群NFT): [github.com/AmazingAng/WTFSolidity](https://github.com/AmazingAng/WTFSolidity) +所有代码和教程开源在github: [github.com/AmazingAng/WTFSolidity](https://github.com/AmazingAng/WTFSolidity) ----- - ## 函数选择器 + 当我们调用智能合约时,本质上是向目标合约发送了一段`calldata`,在remix中发送一次交易后,可以在详细信息中看见`input`即为此次交易的`calldata` ![tx input in remix](./img/29-1.png) @@ -28,32 +28,40 @@ tags: 发送的`calldata`中前4个字节是`selector`(函数选择器)。这一讲,我们将介绍`selector`是什么,以及如何使用。 ### msg.data -`msg.data`是`solidity`中的一个全局变量,值为完整的`calldata`(调用函数时传入的数据)。 + +`msg.data`是`Solidity`中的一个全局变量,值为完整的`calldata`(调用函数时传入的数据)。 在下面的代码中,我们可以通过`Log`事件来输出调用`mint`函数的`calldata`: + ```solidity - // event 返回msg.data - event Log(bytes data); +// event 返回msg.data +event Log(bytes data); - function mint(address to) external{ - emit Log(msg.data); - } +function mint(address to) external{ + emit Log(msg.data); +} ``` + 当参数为`0x2c44b726ADF1963cA47Af88B284C06f30380fC78`时,输出的`calldata`为 -``` + +```text 0x6a6278420000000000000000000000002c44b726adf1963ca47af88b284c06f30380fc78 ``` + 这段很乱的字节码可以分成两部分: -``` + +```text 前4个字节为函数选择器selector: 0x6a627842 后面32个字节为输入的参数: 0x0000000000000000000000002c44b726adf1963ca47af88b284c06f30380fc78 ``` + 其实`calldata`就是告诉智能合约,我要调用哪个函数,以及参数是什么。 ### method id、selector和函数签名 + `method id`定义为`函数签名`的`Keccak`哈希后的前4个字节,当`selector`与`method id`相匹配时,即表示调用该函数,那么`函数签名`是什么? 其实在第21讲中,我们简单介绍了函数签名,为`"函数名(逗号分隔的参数类型)"`。举个例子,上面代码中`mint`的函数签名为`"mint(address)"`。在同一个智能合约中,不同的函数有不同的函数签名,因此我们可以通过函数签名来确定要调用哪个函数。 @@ -61,10 +69,11 @@ tags: **注意**,在函数签名中,`uint`和`int`要写为`uint256`和`int256`。 我们写一个函数,来验证`mint`函数的`method id`是否为`0x6a627842`。大家可以运行下面的函数,看看结果。 + ```solidity - function mintSelector() external pure returns(bytes4 mSelector){ - return bytes4(keccak256("mint(address)")); - } +function mintSelector() external pure returns(bytes4 mSelector){ + return bytes4(keccak256("mint(address)")); +} ``` 结果正是`0x6a627842`: @@ -72,13 +81,14 @@ tags: ![method id in remix](./img/29-2.png) ### 使用selector + 我们可以利用`selector`来调用目标函数。例如我想调用`mint`函数,我只需要利用`abi.encodeWithSelector`将`mint`函数的`method id`作为`selector`和参数打包编码,传给`call`函数: ```solidity - function callWithSignature() external returns(bool, bytes memory){ - (bool success, bytes memory data) = address(this).call(abi.encodeWithSelector(0x6a627842, 0x2c44b726ADF1963cA47Af88B284C06f30380fC78)); - return(success, data); - } +function callWithSignature() external returns(bool, bytes memory){ + (bool success, bytes memory data) = address(this).call(abi.encodeWithSelector(0x6a627842, 0x2c44b726ADF1963cA47Af88B284C06f30380fC78)); + return(success, data); +} ``` 在日志中,我们可以看到`mint`函数被成功调用,并输出`Log`事件。 @@ -86,4 +96,5 @@ tags: ![logs in remix](./img/29-3.png) ## 总结 + 这一讲,我们介绍了什么是`函数选择器`(`selector`),它和`msg.data`、`函数签名`的关系,以及如何使用它调用目标函数。 diff --git a/30_TryCatch/readme.md b/30_TryCatch/readme.md index 65238da55..1f2a2b7de 100644 --- a/30_TryCatch/readme.md +++ b/30_TryCatch/readme.md @@ -9,58 +9,65 @@ tags: # WTF Solidity极简入门: 30. Try Catch -我最近在重新学solidity,巩固一下细节,也写一个“WTF Solidity极简入门”,供小白们使用(编程大佬可以另找教程),每周更新1-3讲。 +我最近在重新学Solidity,巩固一下细节,也写一个“WTF Solidity极简入门”,供小白们使用(编程大佬可以另找教程),每周更新1-3讲。 -欢迎关注我的推特:[@0xAA_Science](https://twitter.com/0xAA_Science) +推特:[@0xAA_Science](https://twitter.com/0xAA_Science) -欢迎加入WTF科学家社区,内有加微信群方法:[链接](https://discord.gg/5akcruXrsk) +社区:[Discord](https://discord.gg/5akcruXrsk)|[微信群](https://docs.google.com/forms/d/e/1FAIpQLSe4KGT8Sh6sJ7hedQRuIYirOoZK_85miz3dw7vA1-YjodgJ-A/viewform?usp=sf_link)|[官网 wtf.academy](https://wtf.academy) -所有代码和教程开源在github(1024个star发课程认证,2048个star发社群NFT): [github.com/AmazingAng/WTFSolidity](https://github.com/AmazingAng/WTFSolidity) +所有代码和教程开源在github: [github.com/AmazingAng/WTFSolidity](https://github.com/AmazingAng/WTFSolidity) ----- -`try-catch`是现代编程语言几乎都有的处理异常的一种标准方式,`solidity`0.6版本也添加了它。这一讲,我们将介绍如何利用`try-catch`处理智能合约中的异常。 +`try-catch`是现代编程语言几乎都有的处理异常的一种标准方式,`Solidity`0.6版本也添加了它。这一讲,我们将介绍如何利用`try-catch`处理智能合约中的异常。 ## `try-catch` -在`solidity`中,`try-catch`只能被用于`external`函数或创建合约时`constructor`(被视为`external`函数)的调用。基本语法如下: + +在`Solidity`中,`try-catch`只能被用于`external`函数或创建合约时`constructor`(被视为`external`函数)的调用。基本语法如下: + ```solidity - try externalContract.f() { - // call成功的情况下 运行一些代码 - } catch { - // call失败的情况下 运行一些代码 - } +try externalContract.f() { + // call成功的情况下 运行一些代码 +} catch { + // call失败的情况下 运行一些代码 +} ``` + 其中`externalContract.f()`是某个外部合约的函数调用,`try`模块在调用成功的情况下运行,而`catch`模块则在调用失败时运行。 同样可以使用`this.f()`来替代`externalContract.f()`,`this.f()`也被视作为外部调用,但不可在构造函数中使用,因为此时合约还未创建。 如果调用的函数有返回值,那么必须在`try`之后声明`returns(returnType val)`,并且在`try`模块中可以使用返回的变量;如果是创建合约,那么返回值是新创建的合约变量。 + ```solidity - try externalContract.f() returns(returnType val){ - // call成功的情况下 运行一些代码 - } catch { - // call失败的情况下 运行一些代码 - } +try externalContract.f() returns(returnType val){ + // call成功的情况下 运行一些代码 +} catch { + // call失败的情况下 运行一些代码 +} ``` 另外,`catch`模块支持捕获特殊的异常原因: ```solidity - try externalContract.f() returns(returnType){ - // call成功的情况下 运行一些代码 - } catch Error(string memory /*reason*/) { - // 捕获revert("reasonString") 和 require(false, "reasonString") - } catch Panic(uint /*errorCode*/) { - // 捕获Panic导致的错误 例如assert失败 溢出 除零 数组访问越界 - } catch (bytes memory /*lowLevelData*/) { - // 如果发生了revert且上面2个异常类型匹配都失败了 会进入该分支 - // 例如revert() require(false) revert自定义类型的error - } +try externalContract.f() returns(returnType){ + // call成功的情况下 运行一些代码 +} catch Error(string memory /*reason*/) { + // 捕获revert("reasonString") 和 require(false, "reasonString") +} catch Panic(uint /*errorCode*/) { + // 捕获Panic导致的错误 例如assert失败 溢出 除零 数组访问越界 +} catch (bytes memory /*lowLevelData*/) { + // 如果发生了revert且上面2个异常类型匹配都失败了 会进入该分支 + // 例如revert() require(false) revert自定义类型的error +} ``` ## `try-catch`实战 + ### `OnlyEven` + 我们创建一个外部合约`OnlyEven`,并使用`try-catch`来处理异常: + ```solidity contract OnlyEven{ constructor(uint a){ @@ -75,94 +82,101 @@ contract OnlyEven{ } } ``` + `OnlyEven`合约包含一个构造函数和一个`onlyEven`函数。 - 构造函数有一个参数`a`,当`a=0`时,`require`会抛出异常;当`a=1`时,`assert`会抛出异常;其他情况均正常。 - `onlyEven`函数有一个参数`b`,当`b`为奇数时,`require`会抛出异常。 ### 处理外部函数调用异常 + 首先,在`TryCatch`合约中定义一些事件和状态变量: + ```solidity - // 成功event - event SuccessEvent(); +// 成功event +event SuccessEvent(); - // 失败event - event CatchEvent(string message); - event CatchByte(bytes data); +// 失败event +event CatchEvent(string message); +event CatchByte(bytes data); - // 声明OnlyEven合约变量 - OnlyEven even; +// 声明OnlyEven合约变量 +OnlyEven even; - constructor() { - even = new OnlyEven(2); - } +constructor() { + even = new OnlyEven(2); +} ``` + `SuccessEvent`是调用成功会释放的事件,而`CatchEvent`和`CatchByte`是抛出异常时会释放的事件,分别对应`require/revert`和`assert`异常的情况。`even`是个`OnlyEven`合约类型的状态变量。 然后我们在`execute`函数中使用`try-catch`处理调用外部函数`onlyEven`中的异常: ```solidity - // 在external call中使用try-catch - function execute(uint amount) external returns (bool success) { - try even.onlyEven(amount) returns(bool _success){ - // call成功的情况下 - emit SuccessEvent(); - return _success; - } catch Error(string memory reason){ - // call不成功的情况下 - emit CatchEvent(reason); - } +// 在external call中使用try-catch +function execute(uint amount) external returns (bool success) { + try even.onlyEven(amount) returns(bool _success){ + // call成功的情况下 + emit SuccessEvent(); + return _success; + } catch Error(string memory reason){ + // call不成功的情况下 + emit CatchEvent(reason); } +} ``` -### 在remix上验证 + +### 在remix上验证,处理外部函数调用异常 当运行`execute(0)`的时候,因为`0`为偶数,满足`require(b % 2 == 0, "Ups! Reverting");`,没有异常抛出,调用成功并释放`SuccessEvent`事件。 -![](./img/30-1.png) +![30-1](./img/30-1.png) 当运行`execute(1)`的时候,因为`1`为奇数,不满足`require(b % 2 == 0, "Ups! Reverting");`,异常抛出,调用失败并释放`CatchEvent`事件。 -![](./img/30-2.png) +![30-2](./img/30-2.png) ### 处理合约创建异常 这里,我们利用`try-catch`来处理合约创建时的异常。只需要把`try`模块改写为`OnlyEven`合约的创建就行: ```solidity - // 在创建新合约中使用try-catch (合约创建被视为external call) - // executeNew(0)会失败并释放`CatchEvent` - // executeNew(1)会失败并释放`CatchByte` - // executeNew(2)会成功并释放`SuccessEvent` - function executeNew(uint a) external returns (bool success) { - try new OnlyEven(a) returns(OnlyEven _even){ - // call成功的情况下 - emit SuccessEvent(); - success = _even.onlyEven(a); - } catch Error(string memory reason) { - // catch失败的 revert() 和 require() - emit CatchEvent(reason); - } catch (bytes memory reason) { - // catch失败的 assert() - emit CatchByte(reason); - } +// 在创建新合约中使用try-catch (合约创建被视为external call) +// executeNew(0)会失败并释放`CatchEvent` +// executeNew(1)会失败并释放`CatchByte` +// executeNew(2)会成功并释放`SuccessEvent` +function executeNew(uint a) external returns (bool success) { + try new OnlyEven(a) returns(OnlyEven _even){ + // call成功的情况下 + emit SuccessEvent(); + success = _even.onlyEven(a); + } catch Error(string memory reason) { + // catch失败的 revert() 和 require() + emit CatchEvent(reason); + } catch (bytes memory reason) { + // catch失败的 assert() + emit CatchByte(reason); } +} ``` -### 在remix上验证 +### 在remix上验证,处理合约创建异常 当运行`executeNew(0)`时,因为`0`不满足`require(a != 0, "invalid number");`,会失败并释放`CatchEvent`事件。 -![](./img/30-3.png) +![30-3](./img/30-3.png) 当运行`executeNew(1)`时,因为`1`不满足`assert(a != 1);`,会失败并释放`CatchByte`事件。 -![](./img/30-4.png) +![30-4](./img/30-4.png) 当运行`executeNew(2)`时,因为`2`满足`require(a != 0, "invalid number");`和`assert(a != 1);`,会成功并释放`SuccessEvent`事件。 -![](./img/30-5.png) +![30-5](./img/30-5.png) ## 总结 -在这一讲,我们介绍了如何在`solidity`使用`try-catch`来处理智能合约运行中的异常: + +在这一讲,我们介绍了如何在`Solidity`使用`try-catch`来处理智能合约运行中的异常: + - 只能用于外部合约调用和合约创建。 - 如果`try`执行成功,返回变量必须声明,并且与返回的变量类型相同。 diff --git a/35_DutchAuction/DutchAuction.sol b/35_DutchAuction/DutchAuction.sol index ce481a59d..90f4e079d 100644 --- a/35_DutchAuction/DutchAuction.sol +++ b/35_DutchAuction/DutchAuction.sol @@ -5,7 +5,7 @@ import "@openzeppelin/contracts/access/Ownable.sol"; import "../34_ERC721/ERC721.sol"; contract DutchAuction is Ownable, ERC721 { - uint256 public constant COLLECTOIN_SIZE = 10000; // NFT总数 + uint256 public constant COLLECTION_SIZE = 10000; // NFT总数 uint256 public constant AUCTION_START_PRICE = 1 ether; // 起拍价 uint256 public constant AUCTION_END_PRICE = 0.1 ether; // 结束价(最低价) uint256 public constant AUCTION_TIME = 10 minutes; // 拍卖时间,为了测试方便设为10分钟 @@ -45,7 +45,7 @@ contract DutchAuction is Ownable, ERC721 { "sale has not started yet" ); // 检查是否设置起拍时间,拍卖是否开始 require( - totalSupply() + quantity <= COLLECTOIN_SIZE, + totalSupply() + quantity <= COLLECTION_SIZE, "not enough remaining reserved for auction to support desired mint amount" ); // 检查是否超过NFT上限 diff --git a/S02_SelectorClash/readme.md b/S02_SelectorClash/readme.md index 0f0c74f76..22aed90f3 100644 --- a/S02_SelectorClash/readme.md +++ b/S02_SelectorClash/readme.md @@ -38,7 +38,7 @@ tags: 1. PowerClash: https://github.com/AmazingAng/power-clash -相比之下,钱包的公钥有`256`字节,被碰撞出来的概率几乎为`0`,非常安全。 +相比之下,钱包的公钥有`64`字节,被碰撞出来的概率几乎为`0`,非常安全。 ## `0xAA` 解决斯芬克斯之谜 diff --git a/S16_NFTReentrancy/readme.md b/S16_NFTReentrancy/readme.md index e63a4af36..c2a7cd16d 100644 --- a/S16_NFTReentrancy/readme.md +++ b/S16_NFTReentrancy/readme.md @@ -21,7 +21,7 @@ tags: ----- -这一讲,我们将介绍NFT合约的重入攻击漏洞,并攻击一个有漏洞的NFT合约,铸造100个NFT。 +这一讲,我们将介绍NFT合约的重入攻击漏洞,并攻击一个有漏洞的NFT合约,铸造10个NFT。 ## NFT重入风险 @@ -88,7 +88,7 @@ contract Attack is IERC721Receiver{ nft.mint(); } - // ERC721的回调函数,会重复调用mint函数,铸造100个 + // ERC721的回调函数,会重复调用mint函数,铸造10个 function onERC721Received(address, address, uint256, bytes memory) public virtual override returns (bytes4) { if(nft.balanceOf(address(this)) < 10){ nft.mint(); @@ -130,4 +130,4 @@ contract Attack is IERC721Receiver{ ## 总结 -这一讲,我们介绍了NFT的重入攻击漏洞,并攻击了一个有漏洞的NFT合约,铸造了100个NFT。目前主要有两种预防重入攻击的办法:检查-影响-交互模式(checks-effect-interaction)和重入锁。 \ No newline at end of file +这一讲,我们介绍了NFT的重入攻击漏洞,并攻击了一个有漏洞的NFT合约,铸造了10个NFT。目前主要有两种预防重入攻击的办法:检查-影响-交互模式(checks-effect-interaction)和重入锁。 \ No newline at end of file