diff --git a/02_ValueTypes/readme.md b/02_ValueTypes/readme.md index 7abfc5698..86fb6f666 100644 --- a/02_ValueTypes/readme.md +++ b/02_ValueTypes/readme.md @@ -68,14 +68,14 @@ bool public _bool5 = _bool != _bool1; // 不相等 ```solidity // 整型 int public _int = -1; // 整数,包括负数 -uint public _uint = 1; // 正整数 -uint256 public _number = 20220330; // 256位正整数 +uint public _uint = 1; // 无符号整数 +uint256 public _number = 20220330; // 256位无符号整数 ``` 常用的整型运算符包括: - 比较运算符(返回布尔值): `<=`, `<`,`==`, `!=`, `>=`, `>` -- 算数运算符: `+`, `-`, `*`, `/`, `%`(取余),`**`(幂) +- 算术运算符: `+`, `-`, `*`, `/`, `%`(取余),`**`(幂) ```solidity // 整数运算 @@ -117,7 +117,7 @@ bytes32 public _byte32 = "MiniSolidity"; bytes1 public _byte = _byte32[0]; ``` -在上述代码中,`MiniSolidity` 变量以字节的方式存储进变量 `_byte32`。如果把它转换成 `16 进制`,就是:`0x4d696e69536f6c69646974790000000000000000000000000000000000000000` +在上述代码中,字符串 `MiniSolidity` 以字节的方式存储进变量 `_byte32`。如果把它转换成 `16 进制`,就是:`0x4d696e69536f6c69646974790000000000000000000000000000000000000000` `_byte` 变量的值为 `_byte32` 的第一个字节,即 `0x4d`。 @@ -132,7 +132,7 @@ enum ActionSet { Buy, Hold, Sell } ActionSet action = ActionSet.Buy; ``` -枚举可以显式地和 `uint` 相互转换,并会检查转换的正整数是否在枚举的长度内,否则会报错: +枚举可以显式地和 `uint` 相互转换,并会检查转换的无符号整数是否在枚举的长度内,否则会报错: ```solidity // enum可以和uint显式的转换 @@ -141,7 +141,7 @@ function enumToUint() external view returns(uint){ } ``` -`enum` 是一个比较冷门的变量,几乎没什么人用。 +`enum` 是一个比较冷门的数据类型,几乎没什么人用。 ## 在 Remix 上运行 diff --git a/03_Function/readme.md b/03_Function/readme.md index 100aa9a27..6577456c5 100644 --- a/03_Function/readme.md +++ b/03_Function/readme.md @@ -25,7 +25,8 @@ Solidity语言的函数非常灵活,可以进行各种复杂操作。在本教 我们先看一下 Solidity 中函数的形式: ```solidity -function () {internal|external|public|private} [pure|view|payable] [returns ()] +function ([parameter types[, ...]]) {internal|external|public|private} [pure|view|payable] [virtual|override] [] +[returns ()]{ } ``` 看着有一些复杂,让我们从前往后逐个解释(方括号中的是可写可不 @@ -35,7 +36,7 @@ function () {internal|external|public|private} [ 2. ``:函数名。 -3. `()`:圆括号内写入函数的参数,即输入到函数的变量类型和名称。 +3. `([parameter types[, ...]])`:圆括号内写入函数的参数,即输入到函数的变量类型和名称。 4. `{internal|external|public|private}`:函数可见性说明符,共有4种。 @@ -46,11 +47,17 @@ function () {internal|external|public|private} [ **注意 1**:合约中定义的函数需要明确指定可见性,它们没有默认值。 - **注意 2**:`public|private|internal` 也可用于修饰状态变量。`public`变量会自动生成同名的`getter`函数,用于查询数值。未标明可见性类型的状态变量,默认为`internal`。 + **注意 2**:`public|private|internal` 也可用于修饰状态变量(定义可参考[WTF Solidity 第5讲的相关内容]([../05_DataStorage/readme.md#1-状态变量](https://github.com/AmazingAng/WTF-Solidity/tree/main/05_DataStorage#1-%E7%8A%B6%E6%80%81%E5%8F%98%E9%87%8F)))。`public`变量会自动生成同名的`getter`函数,用于查询数值。未标明可见性类型的状态变量,默认为`internal`。 5. `[pure|view|payable]`:决定函数权限/功能的关键字。`payable`(可支付的)很好理解,带着它的函数,运行的时候可以给合约转入 ETH。`pure` 和 `view` 的介绍见下一节。 -6. `[returns ()]`:函数返回的变量类型和名称。 +6. `[virtual|override]`: 方法是否可以被重写,或者是否是重写方法。`virtual`用在父合约上,标识的方法可以被子合约重写。`override`用在自合约上,表名方法重写了父合约的方法。 + +7. ``: 自定义的修饰器,可以有0个或多个修饰器。 + +8. `[returns ()]`:函数返回的变量类型和名称。 + +9. ``: 函数体。 ## 到底什么是 `Pure` 和`View`? diff --git a/04_Return/readme.md b/04_Return/readme.md index 4f4ef7a84..863060330 100644 --- a/04_Return/readme.md +++ b/04_Return/readme.md @@ -41,7 +41,7 @@ function returnMultiple() public pure returns(uint256, bool, uint256[3] memory){ ## 命名式返回 -我们可以在 `returns` 中标明返回变量的名称。Solidity 会初始化这些变量,并且自动返回这些函数的值,无需使用 `return`。 +我们可以在 `returns` 中标明返回变量的名称。Solidity 会初始化这些变量,并且自动返回这些变量的值,无需使用 `return`。 ```solidity // 命名式返回 diff --git a/05_DataStorage/readme.md b/05_DataStorage/readme.md index c09414cd9..a53a90e25 100644 --- a/05_DataStorage/readme.md +++ b/05_DataStorage/readme.md @@ -25,7 +25,7 @@ tags: ## 数据位置 -Solidity数据存储位置有三类:`storage`,`memory`和`calldata`。不同存储位置的`gas`成本不同。`storage`类型的数据存在链上,类似计算机的硬盘,消耗`gas`多;`memory`和`calldata`类型的临时存在内存里,消耗`gas`少。大致用法: +Solidity数据存储位置有三类:`storage`,`memory`和`calldata`。不同存储位置的`gas`成本不同。`storage`类型的数据存在链上,类似计算机的硬盘,消耗`gas`多;`memory`和`calldata`类型的临时存在内存里,消耗`gas`少。整体消耗`gas`从多到少依次为:`storage` > `memory` > `calldata`。大致用法: 1. `storage`:合约里的状态变量默认都是`storage`,存储在链上。 @@ -68,7 +68,7 @@ function fCalldata(uint[] calldata _x) public pure returns(uint[] calldata){ ![5-2.png](./img/5-2.png) - `memory`赋值给`memory`,会创建引用,改变新变量会影响原变量。 -- 其他情况下,赋值创建的是本体的副本,即对二者之一的修改,并不会同步到另一方 +- 其他情况下,赋值创建的是本体的副本,即对二者之一的修改,并不会同步到另一方。这有时会涉及到开发中的问题,比如从`storage`中读取数据,赋值给`memory`,然后修改`memory`的数据,但如果没有将`memory`的数据赋值回`storage`,那么`storage`的数据是不会改变的。 ## 变量的作用域 @@ -125,7 +125,7 @@ function global() external view returns(address, uint, bytes memory){ 在上面例子里,我们使用了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 diff --git a/07_Mapping/readme.md b/07_Mapping/readme.md index ee45fa496..e29e6d123 100644 --- a/07_Mapping/readme.md +++ b/07_Mapping/readme.md @@ -61,7 +61,7 @@ mapping(address => address) public swapPair; // 币对的映射,地址到地 - **原理1**: 映射不储存任何键(`Key`)的资讯,也没有length的资讯。 -- **原理2**: 映射使用`keccak256(abi.encodePacked(key, slot))`当成offset存取value,其中`slot`是映射变量定义所在的插槽位置。 +- **原理2**: 对于映射使用`keccak256(h(key) . slot)`计算存取value的位置。感兴趣的可以去阅读 [WTF Solidity 内部规则: 映射存储布局](https://github.com/WTFAcademy/WTF-Solidity-Internals/tree/master/tutorials/02_MappingStorage) - **原理3**: 因为Ethereum会定义所有未使用的空间为0,所以未赋值(`Value`)的键(`Key`)初始值都是各个type的默认值,如uint的默认值是0。 diff --git a/09_Constant/readme.md b/09_Constant/readme.md index 9a475f8c7..79877082e 100644 --- a/09_Constant/readme.md +++ b/09_Constant/readme.md @@ -38,7 +38,7 @@ address constant CONSTANT_ADDRESS = 0x0000000000000000000000000000000000000000; ### immutable -`immutable`变量可以在声明时或构造函数中初始化,因此更加灵活。在`Solidity v8.0.21`以后,`immutable`变量不需要显式初始化,未显式初始化的`immutable`变量将使用数值类型的初始值(见 [8. 变量初始值](https://github.com/AmazingAng/WTF-Solidity/blob/main/08_InitialValue/readme.md#%E5%8F%98%E9%87%8F%E5%88%9D%E5%A7%8B%E5%80%BC))。反之,则需要显式初始化。 +`immutable`变量可以在声明时或构造函数中初始化,因此更加灵活。在`Solidity v0.8.21`以后,`immutable`变量不需要显式初始化,未显式初始化的`immutable`变量将使用数值类型的初始值(见 [8. 变量初始值](https://github.com/AmazingAng/WTF-Solidity/blob/main/08_InitialValue/readme.md#%E5%8F%98%E9%87%8F%E5%88%9D%E5%A7%8B%E5%80%BC))。反之,则需要显式初始化。 若`immutable`变量既在声明时初始化,又在constructor中初始化,会使用constructor初始化的值。 ``` solidity diff --git a/10_InsertionSort/readme.md b/10_InsertionSort/readme.md index b5c56333e..4b463a51e 100644 --- a/10_InsertionSort/readme.md +++ b/10_InsertionSort/readme.md @@ -145,7 +145,7 @@ Remix decoded output 出现错误内容 ### 正确的Solidity插入排序 -花了几个小时,在`Dapp-Learning`社群一个朋友的帮助下,终于找到了`bug`所在。`Solidity`中最常用的变量类型是`uint`,也就是正整数,取到负值的话,会报`underflow`错误。而在插入算法中,变量`j`有可能会取到`-1`,引起报错。 +花了几个小时,在`Dapp-Learning`社群一个朋友的帮助下,终于找到了`bug`所在。`Solidity`中最常用的变量类型是`uint`,也就是无符号整数,取到负值的话,会报`underflow`错误。而在插入算法中,变量`j`有可能会取到`-1`,引起报错。 这里,我们需要把`j`加1,让它无法取到负值。正确代码: diff --git a/12_Event/readme.md b/12_Event/readme.md index 39761e2fa..24ff435dc 100644 --- a/12_Event/readme.md +++ b/12_Event/readme.md @@ -80,6 +80,8 @@ keccak256("Transfer(address,address,uint256)") `indexed`标记的参数可以理解为检索事件的索引“键”,方便之后搜索。每个 `indexed` 参数的大小为固定的256比特,如果参数太大了(比如字符串),就会自动计算哈希存储在主题中。 +这里其实会引入一个新的问题,根据Solidity的[官方文档](https://docs.soliditylang.org/en/v0.8.27/abi-spec.html#encoding-of-indexed-event-parameters), 对于非值类型的参数(如arrays, bytes, strings), Solidity不会直接存储,而是会将`Keccak-256`哈希存储在主题中,从而导致数据信息的丢失。这对于某些依赖于链上事件的DAPP(跨链,用户注册等等)来说,可能会导致事件检索困难,需要解析哈希值。 + ### 数据 `data` 事件中不带 `indexed`的参数会被存储在 `data` 部分中,可以理解为事件的“值”。`data` 部分的变量不能被直接检索,但可以存储任意大小的数据。因此一般 `data` 部分可以用来存储复杂的数据结构,例如数组和字符串等等,因为这些数据超过了256比特,即使存储在事件的 `topics` 部分中,也是以哈希的方式存储。另外,`data` 部分的变量在存储上消耗的gas相比于 `topics` 更少。 diff --git a/13_Inheritance/Inheritance.sol b/13_Inheritance/Inheritance.sol index 83ad1098a..b2c1f1315 100644 --- a/13_Inheritance/Inheritance.sol +++ b/13_Inheritance/Inheritance.sol @@ -5,7 +5,7 @@ pragma solidity ^0.8.21; contract Yeye { event Log(string msg); - // 定义3个function: hip(), pop(), man(),Log值为Yeye。 + // 定义3个function: hip(), pop(), yeye(),Log值为Yeye。 function hip() public virtual{ emit Log("Yeye"); } diff --git a/13_Inheritance/readme.md b/13_Inheritance/readme.md index b6981f4fe..f970386df 100644 --- a/13_Inheritance/readme.md +++ b/13_Inheritance/readme.md @@ -44,7 +44,7 @@ mapping(address => uint256) public override balanceOf; contract Yeye { event Log(string msg); - // 定义3个function: hip(), pop(), man(),Log值为Yeye。 + // 定义3个function: hip(), pop(), yeye(),Log值为Yeye。 function hip() public virtual{ emit Log("Yeye"); } diff --git a/17_Library/readme.md b/17_Library/readme.md index c1e5267e1..84a3515bc 100644 --- a/17_Library/readme.md +++ b/17_Library/readme.md @@ -34,7 +34,7 @@ tags: 3. 不能接收以太币 4. 不可以被销毁 -需要注意的是,库合约重的函数可见性如果被设置为`public`或者`external`,则在调用函数时会触发一次`delegatecall`。而如果被设置为`internal`,则不会引起。对于设置为`private`可见性的函数来说,其仅能在库合约中可见,在其他合约中不可用。 +需要注意的是,库合约中的函数可见性如果被设置为`public`或者`external`,则在调用函数时会触发一次`delegatecall`。而如果被设置为`internal`,则不会引起。对于设置为`private`可见性的函数来说,其仅能在库合约中可见,在其他合约中不可用。 ## Strings库合约 @@ -103,7 +103,7 @@ library Strings { } ``` -他主要包含两个函数,`toString()`将`uint256`转为`string`,`toHexString()`将`uint256`转换为`16进制`,在转换为`string`。 +它主要包含两个函数,`toString()`将`uint256`转换为10进制的`string`,`toHexString()`将`uint256`转换为16进制的`string`。 ### 如何使用库合约 diff --git a/23_Delegatecall/readme.md b/23_Delegatecall/readme.md index f72361183..de60f2c40 100644 --- a/23_Delegatecall/readme.md +++ b/23_Delegatecall/readme.md @@ -83,7 +83,8 @@ contract C { ### 发起调用的合约B -首先,合约`B`必须和目标合约`C`的变量存储布局必须相同,两个变量,并且顺序为`num`和`sender` +首先,合约`B`必须和目标合约`C`的变量存储布局必须相同 —— 即存在两个 `public` 变量且变量类型顺序为 `uint256` 和 `address` +> **注意:** 变量名称可以不同 ```solidity contract B { diff --git a/24_Create/img/24-4.png b/24_Create/img/24-4.png new file mode 100644 index 000000000..ca142f43b Binary files /dev/null and b/24_Create/img/24-4.png differ diff --git a/24_Create/readme.md b/24_Create/readme.md index 2bcaceec9..faaba2d0d 100644 --- a/24_Create/readme.md +++ b/24_Create/readme.md @@ -92,7 +92,7 @@ contract PairFactory{ } ``` -工厂合约(`PairFactory`)有两个状态变量`getPair`是两个代币地址到币对地址的`map`,方便根据代币找到币对地址;`allPairs`是币对地址的数组,存储了所有代币地址。 +工厂合约(`PairFactory`)有两个状态变量`getPair`是两个代币地址到币对地址的`map`,方便根据代币找到币对地址;`allPairs`是币对地址的数组,存储了所有币对地址。 `PairFactory`合约只有一个`createPair`函数,根据输入的两个代币地址`tokenA`和`tokenB`来创建新的`Pair`合约。其中 @@ -112,13 +112,19 @@ BSC链上的PEOPLE地址: 0xbb4CdB9CBd36B01bD1cBaEBF2De08d9173bc095c 1. 使用`WBNB`和`PEOPLE`的地址作为参数调用`createPair`,得到`Pair`合约地址:0xD3e2008b4Da2cD6DEAF73471590fF30C86778A48 ![24-1](./img/24-1.png) -2. 查看`Pair`合约变量 + +2. 将 Contract 改为 `Pair`,然后在 At Address 输入框输入 `Pair` 合约地址,创建一个前端接口用于调用已部署的合约。 + + ![24-4](./img/24-4.png) + +3. 查看`Pair`合约变量 ![24-2](./img/24-2.png) -3. Debug查看`create`操作码 + +4. Debug查看`create`操作码 ![24-3](./img/24-3.png) ## 总结 -这一讲,我们用极简`Uniswap`的例子介绍了如何使用`create`方法再合约里创建合约,下一讲我们将介绍如何使用`create2`方法来实现极简`Uniswap`。 +这一讲,我们用极简`Uniswap`的例子介绍了如何使用`create`方法在合约里创建合约,下一讲我们将介绍如何使用`create2`方法来实现极简`Uniswap`。 diff --git a/26_DeleteContract/readme.md b/26_DeleteContract/readme.md index a097e5b52..0a2140335 100644 --- a/26_DeleteContract/readme.md +++ b/26_DeleteContract/readme.md @@ -34,7 +34,7 @@ tags: `selfdestruct`使用起来非常简单: ```solidity -selfdestruct(_addr); +selfdestruct(_addr); ``` 其中`_addr`是接收合约中剩余`ETH`的地址。`_addr` 地址不需要有`receive()`或`fallback()`也能接收`ETH`。 diff --git a/27_ABIEncode/readme.md b/27_ABIEncode/readme.md index 18d995799..257234d3b 100644 --- a/27_ABIEncode/readme.md +++ b/27_ABIEncode/readme.md @@ -45,11 +45,23 @@ function encode() public view returns(bytes memory result) { } ``` -编码的结果为`0x000000000000000000000000000000000000000000000000000000000000000a0000000000000000000000007a58c0be72be218b41c608b7fe7c5bb630736c7100000000000000000000000000000000000000000000000000000000000000a00000000000000000000000000000000000000000000000000000000000000005000000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000043078414100000000000000000000000000000000000000000000000000000000`,由于`abi.encode`将每个数据都填充为32字节,中间有很多`0`。 +编码的结果为`0x000000000000000000000000000000000000000000000000000000000000000a0000000000000000000000007a58c0be72be218b41c608b7fe7c5bb630736c7100000000000000000000000000000000000000000000000000000000000000a00000000000000000000000000000000000000000000000000000000000000005000000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000043078414100000000000000000000000000000000000000000000000000000000`,详细解释下编码的细节: + +``` +000000000000000000000000000000000000000000000000000000000000000a // x +0000000000000000000000007a58c0be72be218b41c608b7fe7c5bb630736c71 // addr +00000000000000000000000000000000000000000000000000000000000000a0 // name 参数的偏移量 +0000000000000000000000000000000000000000000000000000000000000005 // array[0] +0000000000000000000000000000000000000000000000000000000000000006 // array[1] +0000000000000000000000000000000000000000000000000000000000000004 // name 参数的长度为4字节 +3078414100000000000000000000000000000000000000000000000000000000 // name +``` + +其中 `name` 参数被转换为UTF-8的字节值 `0x30784141`,在 abi 编码规范中,string 属于动态类型 ,动态类型的参数需要借助偏移量进行编码,可以参考[动态类型的使用](https://learnblockchain.cn/docs/solidity/abi-spec.html#id9)。由于 abi.encode 会将每个参与编码的参数元素(包括偏移量,长度)都填充为32字节(evm字长为32字节),所以可以看到编码后的数据中有很多填充的 0 。 ### `abi.encodePacked` -将给定参数根据其所需最低空间编码。它类似 `abi.encode`,但是会把其中填充的很多`0`省略。比如,只用1字节来编码`uint8`类型。当你想省空间,并且不与合约交互的时候,可以使用`abi.encodePacked`,例如算一些数据的`hash`时。 +将给定参数根据其所需最低空间编码。它类似 `abi.encode`,但是会把其中填充的很多`0`省略。比如,只用1字节来编码`uint8`类型。当你想省空间,并且不与合约交互的时候,可以使用`abi.encodePacked`,例如算一些数据的`hash`时。需要注意,`abi.encodePacked`因为不会做填充,所以不同的输入在拼接后可能会产生相同的编码结果,导致冲突,这也带来了潜在的安全风险。 ```solidity function encodePacked() public view returns(bytes memory result) { diff --git a/34_ERC721/ERC721.sol b/34_ERC721/ERC721.sol index d43b33517..f087891c3 100644 --- a/34_ERC721/ERC721.sol +++ b/34_ERC721/ERC721.sol @@ -9,7 +9,7 @@ import "./IERC721Metadata.sol"; import "./String.sol"; contract ERC721 is IERC721, IERC721Metadata{ - using Strings for uint256; // 使用String库, + using Strings for uint256; // 使用Strings库, // Token名称 string public override name; @@ -21,7 +21,7 @@ contract ERC721 is IERC721, IERC721Metadata{ mapping(address => uint) private _balances; // tokenID 到 授权地址 的授权映射 mapping(uint => address) private _tokenApprovals; - // owner地址。到operator地址 的批量授权映射 + // owner地址 到 operator地址 的批量授权映射 mapping(address => mapping(address => bool)) private _operatorApprovals; // 错误 无效的接收者 diff --git a/34_ERC721/readme.md b/34_ERC721/readme.md index 7edb8e14d..c27d8058e 100644 --- a/34_ERC721/readme.md +++ b/34_ERC721/readme.md @@ -210,7 +210,7 @@ import "./IERC721Metadata.sol"; import "./String.sol"; contract ERC721 is IERC721, IERC721Metadata{ - using Strings for uint256; // 使用String库, + using Strings for uint256; // 使用Strings库, // Token名称 string public override name; @@ -222,7 +222,7 @@ contract ERC721 is IERC721, IERC721Metadata{ mapping(address => uint) private _balances; // tokenID 到 授权地址 的授权映射 mapping(uint => address) private _tokenApprovals; - // owner地址。到operator地址 的批量授权映射 + // owner地址 到 operator地址 的批量授权映射 mapping(address => mapping(address => bool)) private _operatorApprovals; // 错误 无效的接收者 @@ -590,7 +590,7 @@ interface ERC721Metadata /* is ERC721 */ { IERC721Metadata.name.selector ^ IERC721Metadata.symbol.selector ^ IERC721Metadata.tokenURI.selector ``` -solamte实现的ERC721.sol是怎么完成这些ERC165要求的特性的呢? +solmate实现的[ERC721.sol](https://github.com/transmissions11/solmate/blob/main/src/tokens/ERC721.sol)是怎么完成这些ERC165要求的特性的呢? ```solidity function supportsInterface(bytes4 interfaceId) public view virtual returns (bool) { diff --git a/35_DutchAuction/readme.md b/35_DutchAuction/readme.md index d563d7e94..31e79a36c 100644 --- a/35_DutchAuction/readme.md +++ b/35_DutchAuction/readme.md @@ -54,7 +54,7 @@ contract DutchAuction is Ownable, ERC721 { 合约中一共有`9`个状态变量,其中有`6`个和拍卖相关,他们是: -- `COLLECTOIN_SIZE`:NFT总量。 +- `COLLECTION_SIZE`:NFT总量。 - `AUCTION_START_PRICE`:荷兰拍卖起拍价,也是最高价。 - `AUCTION_END_PRICE`:荷兰拍卖结束价,也是最低价/地板价。 - `AUCTION_TIME`:拍卖持续时长。 @@ -62,7 +62,7 @@ contract DutchAuction is Ownable, ERC721 { - `auctionStartTime`:拍卖起始时间(区块链时间戳,`block.timestamp`)。 ```solidity - 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分钟 @@ -82,7 +82,7 @@ contract DutchAuction is Ownable, ERC721 { - 设定拍卖起始时间:我们在构造函数中会声明当前区块时间为起始时间,项目方也可以通过`setAuctionStartTime()`函数来调整: ```solidity - constructor() ERC721("WTF Dutch Auctoin", "WTF Dutch Auctoin") { + constructor() ERC721("WTF Dutch Auction", "WTF Dutch Auction") { auctionStartTime = block.timestamp; } @@ -132,7 +132,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/36_MerkleTree/img/36-1.png b/36_MerkleTree/img/36-1.png index 965c2fa9d..ea817988d 100644 Binary files a/36_MerkleTree/img/36-1.png and b/36_MerkleTree/img/36-1.png differ diff --git a/36_MerkleTree/img/36-2.png b/36_MerkleTree/img/36-2.png index acbbba259..dd49a1cd3 100644 Binary files a/36_MerkleTree/img/36-2.png and b/36_MerkleTree/img/36-2.png differ diff --git a/36_MerkleTree/readme.md b/36_MerkleTree/readme.md index 1cf2d5b42..bda7d5851 100644 --- a/36_MerkleTree/readme.md +++ b/36_MerkleTree/readme.md @@ -27,7 +27,7 @@ tags: ![Merkle Tree](./img/36-1.png) -`Merkle Tree`允许对大型数据结构的内容进行有效和安全的验证(`Merkle Proof`)。对于有`N`个叶子结点的`Merkle Tree`,在已知`root`根值的情况下,验证某个数据是否有效(属于`Merkle Tree`叶子结点)只需要`ceil(log₂N)`个数据(也叫`proof`),非常高效。如果数据有误,或者给的`proof`错误,则无法还原出`root`根植。 +`Merkle Tree`允许对大型数据结构的内容进行有效和安全的验证(`Merkle Proof`)。对于有`N`个叶子节点的`Merkle Tree`,在已知`root`根值的情况下,验证某个数据是否有效(属于`Merkle Tree`叶子节点)只需要`ceil(log₂N)`个数据(也叫`proof`),非常高效。如果数据有误,或者给的`proof`错误,则无法还原出`root`根值。 下面的例子中,叶子`L1`的`Merkle proof`为`Hash 0-1`和`Hash 1`:知道这两个值,就能验证`L1`的值是不是在`Merkle Tree`的叶子中。为什么呢? 因为通过叶子`L1`我们就可以算出`Hash 0-0`,我们又知道了`Hash 0-1`,那么`Hash 0-0`和`Hash 0-1`就可以联合算出`Hash 0`,然后我们又知道`Hash 1`,`Hash 0`和`Hash 1`就可以联合算出`Top Hash`,也就是root节点的hash。 @@ -38,7 +38,7 @@ tags: 我们可以利用[网页](https://lab.miguelmota.com/merkletreejs/example/)或者Javascript库[merkletreejs](https://github.com/miguelmota/merkletreejs)来生成`Merkle Tree`。 -这里我们用网页来生成`4`个地址作为叶子结点的`Merkle Tree`。叶子结点输入: +这里我们用网页来生成`4`个地址作为叶子节点的`Merkle Tree`。叶子节点输入: ```solidity [ @@ -63,7 +63,7 @@ tags: ![生成Merkle Tree](./img/36-3.png) ## `Merkle Proof`验证 -通过网站,我们可以得到`地址0`的`proof`如下,即图2中蓝色结点的哈希值: +通过网站,我们可以得到`地址0`的`proof`如下,即图2中蓝色节点的哈希值: ```solidity [ "0x999bf57501565dbd2fdcea36efa2b9aef8340a8901e3459f4a4c926275d36cdb", @@ -140,8 +140,9 @@ contract MerkleTree is ERC721 { { require(_verify(_leaf(account), proof), "Invalid merkle proof"); // Merkle检验通过 require(!mintedAddress[account], "Already minted!"); // 地址没有mint过 - _mint(account, tokenId); // mint + mintedAddress[account] = true; // 记录mint过的地址 + _mint(account, tokenId); // mint } // 计算Merkle树叶子的哈希值 @@ -170,7 +171,7 @@ contract MerkleTree is ERC721 { ### 函数 合约中共有4个函数: - 构造函数:初始化`NFT`的名称和代号,还有`Merkle Tree`的`root`。 -- `mint()`函数:利用白名单铸造`NFT`。参数为白名单地址`account`,铸造的`tokenId`,和`proof`。首先验证`address`是否在白名单中,验证通过则把序号为`tokenId`的`NFT`铸造给该地址,并将它记录到`mintedAddress`。此过程中调用了`_leaf()`和`_verify()`函数。 +- `mint()`函数:利用白名单铸造`NFT`。参数为白名单地址`account`,铸造的`tokenId`,和`proof`。首先验证`address`是否在白名单中,然后验证该地址是否还未铸造,验证通过则先把该地址记录到`mintedAddress`中防止[重入攻击](https://github.com/AmazingAng/WTF-Solidity/blob/main/S01_ReentrancyAttack/readme.md),然后把序号为`tokenId`的`NFT`铸造给该地址。此过程中调用了`_leaf()`和`_verify()`函数。 - `_leaf()`函数:计算了`Merkle Tree`的叶子地址的哈希。 - `_verify()`函数:调用了`MerkleProof`库的`verify()`函数,进行`Merkle Tree`验证。 @@ -185,7 +186,7 @@ merkleroot = 0xeeefd63003e0e702cb41cd0043015a6e26ddb38073cc6ffeb0ba3e808ba8c097 ![部署MerkleTree合约](./img/36-5.png) -接下来运行`mint`函数给地址0铸造`NFT`,`3`个参数分别为: +接下来运行`mint`函数给`地址0`铸造`NFT`,`3`个参数分别为: ```solidity account = 0x5B38Da6a701c568545dCfcB03FcB875f56beddC4 @@ -195,7 +196,7 @@ proof = [ "0x999bf57501565dbd2fdcea36efa2b9aef8340a8901e3459f4a4c926275d36cdb" ![白名单mint](./img/36-6.png) -我们可以用`ownerOf`函数验证`tokenId`为0的`NFT`已经铸造给了地址0,合约运行成功! +我们可以用`ownerOf`函数验证`tokenId`为0的`NFT` 已经铸造给了`地址0`,合约运行成功! ![tokenId为0的持有者改变,合约运行成功!](./img/36-7.png) diff --git a/37_Signature/readme.md b/37_Signature/readme.md index 4f6fa1bd3..bba4ec547 100644 --- a/37_Signature/readme.md +++ b/37_Signature/readme.md @@ -137,7 +137,7 @@ print(f"签名:{signed_message['signature'].hex()}") 为了验证签名,验证者需要拥有`消息`,`签名`,和签名使用的`公钥`。我们能验证签名的原因是只有`私钥`的持有者才能够针对交易生成这样的签名,而别人不能。 -**4. 通过签名和消息恢复公钥:**`签名`是由数学算法生成的。这里我们使用的是`rsv签名`,`签名`中包含`r, s, v`三个值的信息。而后,我们可以通过`r, s, v`及`以太坊签名消息`来求得`公钥`。下面的`recoverSigner()`函数实现了上述步骤,它利用`以太坊签名消息 _msgHash`和`签名 _signature`恢复`公钥`(使用了简单的内联汇编): +**4. 通过签名和消息恢复公钥:**`签名`是由数学算法生成的。这里我们使用的是`rsv签名`,`签名`中包含`r, s, v`三个值的信息,长度分别为32 bytes,32 bytes,1 byte。而后,我们可以通过`r, s, v`及`以太坊签名消息`来求得`公钥`。下面的`recoverSigner()`函数实现了上述步骤,它利用`以太坊签名消息 _msgHash`和`签名 _signature`恢复`公钥`(使用了简单的内联汇编): ```solidity // @dev 从_msgHash和签名_signature中恢复signer地址 @@ -171,6 +171,9 @@ print(f"签名:{signed_message['signature'].hex()}") _msgHash:0xb42ca4636f721c7a331923e764587e98ec577cea1a185f60dfcc14dbb9bd900b _signature:0x390d704d7ab732ce034203599ee93dd5d3cb0d4d1d7c600ac11726659489773d559b12d220f99f41d17651b0c1c6a669d346a397f8541760d6b32a5725378b241c ``` + +需要注意的是,这里需要对输入参数`_signature`的长度进行检查,确保其长度为65bytes,否则会产生签名重放问题。具体问题可以参考[BlazCTF中的Cyber Cartel](https://github.com/DeFiHackLabs/blazctf-2024-writeup/blob/main/writeup/cyber-cartel.md). + ![通过签名和消息恢复公钥](./img/37-8.png) **5. 对比公钥并验证签名:** 接下来,我们只需要比对恢复的`公钥`与签名者公钥`_signer`是否相等:若相等,则签名有效;否则,签名无效: diff --git a/38_NFTSwap/NFTSwap.sol b/38_NFTSwap/NFTSwap.sol index 0483d73ce..4a1bc170f 100644 --- a/38_NFTSwap/NFTSwap.sol +++ b/38_NFTSwap/NFTSwap.sol @@ -46,7 +46,7 @@ contract NFTSwap is IERC721Receiver { require(_nft.getApproved(_tokenId) == address(this), "Need Approval"); // 合约得到授权 require(_price > 0); // 价格大于0 - Order storage _order = nftList[_nftAddr][_tokenId]; //设置NF持有人和价格 + Order storage _order = nftList[_nftAddr][_tokenId]; //设置NFT持有人和价格 _order.owner = msg.sender; _order.price = _price; // 将NFT转账到合约 @@ -68,13 +68,15 @@ contract NFTSwap is IERC721Receiver { // 将NFT转给买家 _nft.safeTransferFrom(address(this), msg.sender, _tokenId); // 将ETH转给卖家,多余ETH给买家退款 - payable(_order.owner).transfer(_order.price); - payable(msg.sender).transfer(msg.value - _order.price); - - delete nftList[_nftAddr][_tokenId]; // 删除order + if (msg.value > _order.price) { + payable(_order.owner).transfer(_order.price); + payable(msg.sender).transfer(msg.value - _order.price); + } // 释放Purchase事件 emit Purchase(msg.sender, _nftAddr, _tokenId, _order.price); + + delete nftList[_nftAddr][_tokenId]; // 删除order } // 撤单: 卖家取消挂单 diff --git a/38_NFTSwap/readme.md b/38_NFTSwap/readme.md index f578561c2..147cdaf9c 100644 --- a/38_NFTSwap/readme.md +++ b/38_NFTSwap/readme.md @@ -20,7 +20,7 @@ tags: --- -`Opensea`是以太坊上最大的`NFT`交易平台,总交易总量达到了`$300亿`。`Opensea`在交易中抽成`2.5%`,因此它通过用户交易至少获利了`$7.5亿`。另外,它的运作并不去中心化,且不准备发币补偿用户。`NFT`玩家苦`Opensea`久已,今天我们就利用智能合约搭建一个零手续费的去中心化`NFT`交易所:`NFTSwap`。 +`OpenSea`是以太坊上最大的`NFT`交易平台,总交易总量达到了`$300亿`。`OpenSea`在交易中抽成`2.5%`,因此它通过用户交易至少获利了`$7.5亿`。另外,它的运作并不去中心化,且不准备发币补偿用户。`NFT`玩家苦`OpenSea`久已,今天我们就利用智能合约搭建一个零手续费的去中心化`NFT`交易所:`NFTSwap`。 ## 设计逻辑 @@ -31,31 +31,36 @@ tags: ## `NFTSwap`合约 ### 事件 + 合约包含`4`个事件,对应挂单`list`、撤单`revoke`、修改价格`update`、购买`purchase`这四个行为: + ``` solidity - event List(address indexed seller, address indexed nftAddr, uint256 indexed tokenId, uint256 price); - event Purchase(address indexed buyer, address indexed nftAddr, uint256 indexed tokenId, uint256 price); - event Revoke(address indexed seller, address indexed nftAddr, uint256 indexed tokenId); - event Update(address indexed seller, address indexed nftAddr, uint256 indexed tokenId, uint256 newPrice); +event List(address indexed seller, address indexed nftAddr, uint256 indexed tokenId, uint256 price); +event Purchase(address indexed buyer, address indexed nftAddr, uint256 indexed tokenId, uint256 price); +event Revoke(address indexed seller, address indexed nftAddr, uint256 indexed tokenId); +event Update(address indexed seller, address indexed nftAddr, uint256 indexed tokenId, uint256 newPrice); ``` ### 订单 + `NFT`订单抽象为`Order`结构体,包含挂单价格`price`和持有人`owner`信息。`nftList`映射记录了订单是对应的`NFT`系列(合约地址)和`tokenId`信息。 + ```solidity - // 定义order结构体 - struct Order{ - address owner; - uint256 price; - } - // NFT Order映射 - mapping(address => mapping(uint256 => Order)) public nftList; +// 定义order结构体 +struct Order{ + address owner; + uint256 price; +} +// NFT Order映射 +mapping(address => mapping(uint256 => Order)) public nftList; ``` ### 回退函数 -在`NFTSwap`中,用户使用`ETH`购买`NFT`。因此,合约需要实现`fallback()`函数来接收`ETH`。 + +在`NFTSwap`合约中,用户使用`ETH`购买`NFT`。因此,合约需要实现`fallback()`函数来接收`ETH`。 ```solidity - fallback() external payable{} +fallback() external payable{} ``` ### onERC721Received @@ -63,8 +68,7 @@ tags: `ERC721`的安全转账函数会检查接收合约是否实现了`onERC721Received()`函数,并返回正确的选择器`selector`。用户下单之后,需要将`NFT`发送给`NFTSwap`合约。因此`NFTSwap`继承`IERC721Receiver`接口,并实现`onERC721Received()`函数: ```solidity -contract NFTSwap is IERC721Receiver{ - +contract NFTSwap is IERC721Receiver { // 实现{IERC721Receiver}的onERC721Received,能够接收ERC721代币 function onERC721Received( address operator, @@ -74,6 +78,7 @@ contract NFTSwap is IERC721Receiver{ ) external override returns (bytes4){ return IERC721Receiver.onERC721Received.selector; } +} ``` ### 交易 @@ -82,14 +87,14 @@ contract NFTSwap is IERC721Receiver{ - 挂单`list()`:卖家创建`NFT`并创建订单,并释放`List`事件。参数为`NFT`合约地址`_nftAddr`,`NFT`对应的`_tokenId`,挂单价格`_price`(**注意:单位是`wei`**)。成功后,`NFT`会从卖家转到`NFTSwap`合约中。 -```solidity + ```solidity // 挂单: 卖家上架NFT,合约地址为_nftAddr,tokenId为_tokenId,价格_price为以太坊(单位是wei) function list(address _nftAddr, uint256 _tokenId, uint256 _price) public{ IERC721 _nft = IERC721(_nftAddr); // 声明IERC721接口合约变量 require(_nft.getApproved(_tokenId) == address(this), "Need Approval"); // 合约得到授权 require(_price > 0); // 价格大于0 - Order storage _order = nftList[_nftAddr][_tokenId]; //设置NF持有人和价格 + Order storage _order = nftList[_nftAddr][_tokenId]; //设置NFT持有人和价格 _order.owner = msg.sender; _order.price = _price; // 将NFT转账到合约 @@ -98,72 +103,80 @@ contract NFTSwap is IERC721Receiver{ // 释放List事件 emit List(msg.sender, _nftAddr, _tokenId, _price); } -``` + ``` + - 撤单`revoke()`:卖家撤回挂单,并释放`Revoke`事件。参数为`NFT`合约地址`_nftAddr`,`NFT`对应的`_tokenId`。成功后,`NFT`会从`NFTSwap`合约转回卖家。 -```solidity - // 撤单: 卖家取消挂单 - function revoke(address _nftAddr, uint256 _tokenId) public { - Order storage _order = nftList[_nftAddr][_tokenId]; // 取得Order - require(_order.owner == msg.sender, "Not Owner"); // 必须由持有人发起 - // 声明IERC721接口合约变量 - IERC721 _nft = IERC721(_nftAddr); - require(_nft.ownerOf(_tokenId) == address(this), "Invalid Order"); // NFT在合约中 - - // 将NFT转给卖家 - _nft.safeTransferFrom(address(this), msg.sender, _tokenId); - delete nftList[_nftAddr][_tokenId]; // 删除order + + ```solidity + // 撤单: 卖家取消挂单 + function revoke(address _nftAddr, uint256 _tokenId) public { + Order storage _order = nftList[_nftAddr][_tokenId]; // 取得Order + require(_order.owner == msg.sender, "Not Owner"); // 必须由持有人发起 + // 声明IERC721接口合约变量 + IERC721 _nft = IERC721(_nftAddr); + require(_nft.ownerOf(_tokenId) == address(this), "Invalid Order"); // NFT在合约中 - // 释放Revoke事件 - emit Revoke(msg.sender, _nftAddr, _tokenId); - } -``` + // 将NFT转给卖家 + _nft.safeTransferFrom(address(this), msg.sender, _tokenId); + delete nftList[_nftAddr][_tokenId]; // 删除order + + // 释放Revoke事件 + emit Revoke(msg.sender, _nftAddr, _tokenId); + } + ``` + - 修改价格`update()`:卖家修改`NFT`订单价格,并释放`Update`事件。参数为`NFT`合约地址`_nftAddr`,`NFT`对应的`_tokenId`,更新后的挂单价格`_newPrice`(**注意:单位是`wei`**)。 -```solidity - // 调整价格: 卖家调整挂单价格 - function update(address _nftAddr, uint256 _tokenId, uint256 _newPrice) public { - require(_newPrice > 0, "Invalid Price"); // NFT价格大于0 - Order storage _order = nftList[_nftAddr][_tokenId]; // 取得Order - require(_order.owner == msg.sender, "Not Owner"); // 必须由持有人发起 - // 声明IERC721接口合约变量 - IERC721 _nft = IERC721(_nftAddr); - require(_nft.ownerOf(_tokenId) == address(this), "Invalid Order"); // NFT在合约中 - - // 调整NFT价格 - _order.price = _newPrice; - - // 释放Update事件 - emit Update(msg.sender, _nftAddr, _tokenId, _newPrice); - } -``` -- 购买`purchase`:买家支付`ETH`购买挂单的`NFT`,并释放`Purchase`事件。参数为`NFT`合约地址`_nftAddr`,`NFT`对应的`_tokenId`。成功后,`ETH`将转给卖家,`NFT`将从`NFTSwap`合约转给买家。 -```solidity - // 购买: 买家购买NFT,合约为_nftAddr,tokenId为_tokenId,调用函数时要附带ETH - function purchase(address _nftAddr, uint256 _tokenId) payable public { - Order storage _order = nftList[_nftAddr][_tokenId]; // 取得Order - require(_order.price > 0, "Invalid Price"); // NFT价格大于0 - require(msg.value >= _order.price, "Increase price"); // 购买价格大于标价 - // 声明IERC721接口合约变量 - IERC721 _nft = IERC721(_nftAddr); - require(_nft.ownerOf(_tokenId) == address(this), "Invalid Order"); // NFT在合约中 - - // 将NFT转给买家 - _nft.safeTransferFrom(address(this), msg.sender, _tokenId); - // 将ETH转给卖家,多余ETH给买家退款 - payable(_order.owner).transfer(_order.price); - payable(msg.sender).transfer(msg.value-_order.price); - - delete nftList[_nftAddr][_tokenId]; // 删除order - - // 释放Purchase事件 - emit Purchase(msg.sender, _nftAddr, _tokenId, _order.price); - } -``` + ```solidity + // 调整价格: 卖家调整挂单价格 + function update(address _nftAddr, uint256 _tokenId, uint256 _newPrice) public { + require(_newPrice > 0, "Invalid Price"); // NFT价格大于0 + Order storage _order = nftList[_nftAddr][_tokenId]; // 取得Order + require(_order.owner == msg.sender, "Not Owner"); // 必须由持有人发起 + // 声明IERC721接口合约变量 + IERC721 _nft = IERC721(_nftAddr); + require(_nft.ownerOf(_tokenId) == address(this), "Invalid Order"); // NFT在合约中 + + // 调整NFT价格 + _order.price = _newPrice; + + // 释放Update事件 + emit Update(msg.sender, _nftAddr, _tokenId, _newPrice); + } + ``` + +- 购买`purchase()`:买家支付`ETH`购买挂单的`NFT`,并释放`Purchase`事件。参数为`NFT`合约地址`_nftAddr`,`NFT`对应的`_tokenId`。成功后,`ETH`将转给卖家,`NFT`将从`NFTSwap`合约转给买家。 + + ```solidity + // 购买: 买家购买NFT,合约为_nftAddr,tokenId为_tokenId,调用函数时要附带ETH + function purchase(address _nftAddr, uint256 _tokenId) public payable { + Order storage _order = nftList[_nftAddr][_tokenId]; // 取得Order + require(_order.price > 0, "Invalid Price"); // NFT价格大于0 + require(msg.value >= _order.price, "Increase price"); // 购买价格大于标价 + // 声明IERC721接口合约变量 + IERC721 _nft = IERC721(_nftAddr); + require(_nft.ownerOf(_tokenId) == address(this), "Invalid Order"); // NFT在合约中 + + // 将NFT转给买家 + _nft.safeTransferFrom(address(this), msg.sender, _tokenId); + // 将ETH转给卖家,多余ETH给买家退款 + if (msg.value > _order.price) { + payable(_order.owner).transfer(_order.price); + payable(msg.sender).transfer(msg.value - _order.price); + } + + // 释放Purchase事件 + emit Purchase(msg.sender, _nftAddr, _tokenId, _order.price); + + delete nftList[_nftAddr][_tokenId]; // 删除order + } + ``` ## `Remix`实现 ### 1. 部署NFT合约 + 参考 [ERC721](https://github.com/AmazingAng/WTF-Solidity/tree/main/34_ERC721) 教程了解NFT,并部署`WTFApe` NFT合约。 ![部署NFT合约](./img/38-1.png) @@ -189,11 +202,13 @@ contract NFTSwap is IERC721Receiver{ 按照上述方法,将TokenId为 `0` 和 `1` 的NFT都mint给自己,其中`tokenId`为`0`的,我们执行更新购买操作,`tokenId`为`1`的,我们执行下架操作。 ### 2. 部署`NFTSwap`合约 + 部署`NFTSwap`合约。 ![部署`NFTSwap`合约](./img/38-4.png) ### 3. 将要上架的`NFT`授权给`NFTSwap`合约 + 在`WTFApe`合约中调用 `approve()`授权函数,将自己持有的`tokenId`为0的NFT授权给`NFTSwap`合约地址。 `approve(address to, uint tokenId)`方法有2个参数: @@ -202,11 +217,12 @@ contract NFTSwap is IERC721Receiver{ `tokenId`: `tokenId`为NFT的id,本案例中为上述mint的`0`Id。 -![](./img/38-5.png) +![将要上架的`NFT`授权给`NFTSwap`合约](./img/38-5.png) 按照上述方法,同理将`tokenId`为`1`的NFT也授权给`NFTSwap`合约地址。 ### 4. 上架`NFT` + 调用`NFTSwap`合约的`list()`函数,将自己持有的`tokenId`为0的NFT上架到`NFTSwap`,价格设为1 `wei`。 `list(address _nftAddr, uint256 _tokenId, uint256 _price)`方法有3个参数: @@ -217,7 +233,7 @@ contract NFTSwap is IERC721Receiver{ `_price`: `_price`为NFT的价格,本案例中为1 `wei`。 -![](./img/38-6.png) +![上架`NFT`](./img/38-6.png) 按照上述方法,同理将自己持有的`tokenId`为1的NFT上架到`NFTSwap`,价格设为1 `wei`。 @@ -227,9 +243,9 @@ contract NFTSwap is IERC721Receiver{ `nftList`:是一个NFT Order的映射,结构如下: -`nftList[_nftAddr][_tokenId]`: 输入`_nftAddr`和`_tokenId`,返回一个NFT订单。 +`nftList[_nftAddr][_tokenId]`: 输入`_nftAddr`和`_tokenId`,返回一个NFT订单。 -![](./img/38-7.png) +![查看上架NFT](./img/38-7.png) ### 6. 更新`NFT`价格 @@ -245,8 +261,7 @@ contract NFTSwap is IERC721Receiver{ 执行`update`之后,调用`nftList` 查看更新后的价格 -![](./img/38-8.png) - +![更新`NFT`价格](./img/38-8.png) ### 5. 下架NFT @@ -260,12 +275,11 @@ contract NFTSwap is IERC721Receiver{ `_tokenId`: `_tokenId`为NFT的id,本案例中为上述mint的`1`Id。 -![](./img/38-9.png) +![下架NFT](./img/38-9.png) 调用`NFTSwap`合约的`nftList()`函数,可以看到`NFT`已经下架。再次上架需要重新授权。 -![](./img/38-10.png) - +![验证是否下架](./img/38-10.png) **注意下架NFT之后,需要重新从步骤3开始,重新授权和上架NFT之后,才能进行购买** ### 6. 购买`NFT` @@ -282,13 +296,14 @@ contract NFTSwap is IERC721Receiver{ `_wei`: `_wei`为支付的`ETH`数量,本案例中为77 `wei`。 -![](./img/38-11.png) +![购买`NFT`](./img/38-11.png) ### 7. 验证`NFT`持有人改变 购买成功之后,调用`WTFApe`合约的`ownerOf()`函数,可以看到`NFT`持有者发生变化,购买成功! -![](./img/38-12.png) +![验证`NFT`持有人改变](./img/38-12.png) ## 总结 + 这一讲,我们建立了一个零手续费的去中心化`NFT`交易所。`OpenSea`虽然对`NFT`的发展做了很大贡献,但它的缺点也非常明显:高手续费、不发币回馈用户、交易机制容易被钓鱼导致用户资产丢失。目前`Looksrare`和`dydx`等新的`NFT`交易平台正在挑战`OpenSea`的位置,`Uniswap`也在研究新的`NFT`交易所。相信不久的将来,我们会用到更好的`NFT`交易所。 diff --git a/40_ERC1155/ERC1155.sol b/40_ERC1155/ERC1155.sol index 21dabe48e..63bcf180c 100644 --- a/40_ERC1155/ERC1155.sol +++ b/40_ERC1155/ERC1155.sol @@ -12,7 +12,6 @@ import "../34_ERC721/IERC165.sol"; * 见 https://eips.ethereum.org/EIPS/eip-1155 */ contract ERC1155 is IERC165, IERC1155, IERC1155MetadataURI { - using Strings for uint256; // 使用String库 // Token名称 string public name; // Token代号 diff --git a/40_ERC1155/readme.md b/40_ERC1155/readme.md index 7b38cb70a..c852904bc 100644 --- a/40_ERC1155/readme.md +++ b/40_ERC1155/readme.md @@ -248,7 +248,6 @@ import "https://github.com/AmazingAng/WTF-Solidity/blob/main/34_ERC721/IERC165.s * 见 https://eips.ethereum.org/EIPS/eip-1155 */ contract ERC1155 is IERC165, IERC1155, IERC1155MetadataURI { - using Strings for uint256; // 使用String库 // Token名称 string public name; // Token代号 diff --git a/46_ProxyContract/readme.md b/46_ProxyContract/readme.md index e5415c7c4..1836886c9 100644 --- a/46_ProxyContract/readme.md +++ b/46_ProxyContract/readme.md @@ -115,7 +115,7 @@ fallback() external payable { - `implementation`:占位变量,与`Proxy`合约保持一致,防止插槽冲突。 - `x`:`uint`变量,被设置为`99`。 - `CallSuccess`事件:在调用成功时释放。 -- `increment()`函数:会被`Proxy`合约调用,释放`CallSuccess`事件,并返回一个`uint`,它的`selector`为`0xd09de08a`。如果直接调用`increment()`回返回`100`,但是通过`Proxy`调用它会返回`1`,大家可以想想为什么? +- `increment()`函数:会被`Proxy`合约调用,释放`CallSuccess`事件,并返回一个`uint`,它的`selector`为`0xd09de08a`。如果直接调用`increment()`会返回`100`,但是通过`Proxy`调用它会返回`1`,大家可以想想为什么? ```solidity /** diff --git a/47_Upgrade/readme.md b/47_Upgrade/readme.md index 970b91c5e..26eb6df2f 100644 --- a/47_Upgrade/readme.md +++ b/47_Upgrade/readme.md @@ -79,7 +79,7 @@ contract SimpleUpgrade { ### 旧逻辑合约 -这个逻辑合约包含`3`个状态变量,与保持代理合约一致,防止插槽冲突。它只有一个函数`foo()`,将代理合约中的`words`的值改为`"old"`。 +这个逻辑合约包含`3`个状态变量,与代理合约保持一致,防止插槽冲突。它只有一个函数`foo()`,将代理合约中的`words`的值改为`"old"`。 ```solidity // 逻辑合约1 @@ -98,7 +98,7 @@ contract Logic1 { ### 新逻辑合约 -这个逻辑合约包含`3`个状态变量,与保持代理合约一致,防止插槽冲突。它只有一个函数`foo()`,将代理合约中的`words`的值改为`"new"`。 +这个逻辑合约包含`3`个状态变量,与代理合约保持一致,防止插槽冲突。它只有一个函数`foo()`,将代理合约中的`words`的值改为`"new"`。 ```solidity // 逻辑合约2 diff --git a/48_TransparentProxy/readme.md b/48_TransparentProxy/readme.md index 3c059ec29..f69f4afa1 100644 --- a/48_TransparentProxy/readme.md +++ b/48_TransparentProxy/readme.md @@ -95,7 +95,7 @@ contract TransparentProxy { ### 逻辑合约 -这里的新、旧逻辑合约与[第47讲](https://github.com/AmazingAng/WTF-Solidity/blob/main/47_Upgrade/readme.md)一样。逻辑合约包含`3`个状态变量,与保持代理合约一致,防止插槽冲突;包含一个函数`foo()`,旧逻辑合约会将`words`的值改为`"old"`,新的会改为`"new"`。 +这里的新、旧逻辑合约与[第47讲](https://github.com/AmazingAng/WTF-Solidity/blob/main/47_Upgrade/readme.md)一样。逻辑合约包含`3`个状态变量,与代理合约保持一致,防止插槽冲突;包含一个函数`foo()`,旧逻辑合约会将`words`的值改为`"old"`,新的会改为`"new"`。 ```solidity // 旧逻辑合约 diff --git a/49_UUPS/readme.md b/49_UUPS/readme.md index 14fa0408a..1c4a3d63d 100644 --- a/49_UUPS/readme.md +++ b/49_UUPS/readme.md @@ -70,7 +70,7 @@ contract UUPSProxy { ### UUPS的逻辑合约 -UUPS的逻辑合约与[第47讲](https://github.com/AmazingAng/WTF-Solidity/blob/main/47_Upgrade/readme.md)中的不同是多了个升级函数。UUPS逻辑合约包含`3`个状态变量,与保持代理合约一致,防止插槽冲突。它包含`2`个 +UUPS的逻辑合约与[第47讲](https://github.com/AmazingAng/WTF-Solidity/blob/main/47_Upgrade/readme.md)中的不同是多了个升级函数。UUPS逻辑合约包含`3`个状态变量,与代理合约保持一致,防止插槽冲突。它包含`2`个 - `upgrade()`:升级函数,将改变逻辑合约地址`implementation`,只能由`admin`调用。 - `foo()`:旧UUPS逻辑合约会将`words`的值改为`"old"`,新的会改为`"new"`。 diff --git a/50_MultisigWallet/MultisigWallet.sol b/50_MultisigWallet/MultisigWallet.sol index 481828f84..71c28d209 100644 --- a/50_MultisigWallet/MultisigWallet.sol +++ b/50_MultisigWallet/MultisigWallet.sol @@ -28,7 +28,7 @@ contract MultisigWallet { function _setupOwners(address[] memory _owners, uint256 _threshold) internal { // threshold没被初始化过 require(threshold == 0, "WTF5000"); - // 多签执行门槛 小于 多签人数 + // 多签执行门槛 小于或等于 多签人数 require(_threshold <= _owners.length, "WTF5001"); // 多签执行门槛至少为1 require(_threshold >= 1, "WTF5002"); diff --git a/50_MultisigWallet/readme.md b/50_MultisigWallet/readme.md index ba4eaf5d0..45d2192ff 100644 --- a/50_MultisigWallet/readme.md +++ b/50_MultisigWallet/readme.md @@ -43,7 +43,7 @@ Gnosis Safe多签钱包是以太坊最流行的多签钱包,管理近400亿美 - `nonce`:初始为`0`,随着多签合约每笔成功执行的交易递增的值,可以防止签名重放攻击。 - `chainid`:链id,防止不同链的签名重放攻击。 -3. 收集多签签名(链下):将上一步的交易ABI编码并计算哈希,得到交易哈希,然后让多签人签名,并拼接到一起的到打包签名。对ABI编码和哈希不了解的,可以看WTF Solidity极简教程[第27讲](https://github.com/AmazingAng/WTF-Solidity/blob/main/27_ABIEncode/readme.md)和[第28讲](https://github.com/AmazingAng/WTF-Solidity/blob/main/28_Hash/readme.md)。 +3. 收集多签签名(链下):将上一步的交易ABI编码并计算哈希,得到交易哈希,然后让多签人签名,并拼接到一起得到打包签名。对ABI编码和哈希不了解的,可以看WTF Solidity极简教程[第27讲](https://github.com/AmazingAng/WTF-Solidity/blob/main/27_ABIEncode/readme.md)和[第28讲](https://github.com/AmazingAng/WTF-Solidity/blob/main/28_Hash/readme.md)。 ```solidity 交易哈希: 0xc1b055cf8e78338db21407b425114a2e258b0318879327945b661bfdea570e66 @@ -107,7 +107,7 @@ Gnosis Safe多签钱包是以太坊最流行的多签钱包,管理近400亿美 function _setupOwners(address[] memory _owners, uint256 _threshold) internal { // threshold没被初始化过 require(threshold == 0, "WTF5000"); - // 多签执行门槛 小于 多签人数 + // 多签执行门槛 小于或等于 多签人数 require(_threshold <= _owners.length, "WTF5001"); // 多签执行门槛至少为1 require(_threshold >= 1, "WTF5002"); @@ -150,7 +150,7 @@ Gnosis Safe多签钱包是以太坊最流行的多签钱包,管理近400亿美 } ``` -4. `checkSignatures()`:检查签名和交易数据的哈希是否对应,数量是否达到门槛,若否,交易会revert。单个签名长度为65字节,因此打包签名的长度要长于`threshold * 65`。调用了`signatureSplit()`分离出单个签名。这个函数的大致思路: +4. `checkSignatures()`:检查签名和交易数据的哈希是否对应,数量是否达到门槛,若否,交易会revert。单个签名长度为65字节,因此打包签名的长度要长于或等于`threshold * 65`。调用了`signatureSplit()`分离出单个签名。这个函数的大致思路: - 用ecdsa获取签名地址. - 利用 `currentOwner > lastOwner` 确定签名来自不同多签(多签地址递增)。 - 利用`isOwner[currentOwner]`确定签名者为多签持有人。 @@ -286,7 +286,7 @@ Gnosis Safe多签钱包是以太坊最流行的多签钱包,管理近400亿美 多签地址2的签名: 0x6b228b6033c097e220575f826560226a5855112af667e984aceca50b776f4c885e983f1f2155c294c86a905977853c6b1bb630c488502abcc838f9a225c813811c - 讲两个签名拼接到一起,得到打包签名: 0xa3f3e4375f54ad0a8070f5abd64e974b9b84306ac0dd5f59834efc60aede7c84454813efd16923f1a8c320c05f185bd90145fd7a7b741a8d13d4e65a4722687e1b6b228b6033c097e220575f826560226a5855112af667e984aceca50b776f4c885e983f1f2155c294c86a905977853c6b1bb630c488502abcc838f9a225c813811c + 将两个签名拼接到一起,得到打包签名: 0xa3f3e4375f54ad0a8070f5abd64e974b9b84306ac0dd5f59834efc60aede7c84454813efd16923f1a8c320c05f185bd90145fd7a7b741a8d13d4e65a4722687e1b6b228b6033c097e220575f826560226a5855112af667e984aceca50b776f4c885e983f1f2155c294c86a905977853c6b1bb630c488502abcc838f9a225c813811c ``` ![签名](./img/50-5.png) diff --git a/51_ERC4626/readme.md b/51_ERC4626/readme.md index 90062a314..60b66eb43 100644 --- a/51_ERC4626/readme.md +++ b/51_ERC4626/readme.md @@ -47,7 +47,7 @@ tags: ### ERC4626 要点 -ERC4626 标准主要实现了一下几个逻辑: +ERC4626 标准主要实现了以下几个逻辑: 1. ERC20: ERC4626 继承了 ERC20,金库份额就是用 ERC20 代币代表的:用户将特定的 ERC20 基础资产(比如 WETH)存进金库,合约会给他铸造特定数量的金库份额代币;当用户从金库中提取基础资产时,会销毁相应数量的金库份额代币。`asset()` 函数会返回金库的基础资产的代币地址。 2. 存款逻辑:让用户存入基础资产,并铸造相应数量的金库份额。相关函数为 `deposit()` 和 `mint()`。`deposit(uint assets, address receiver)` 函数让用户存入 `assets` 单位的资产,并铸造相应数量的金库份额给 `receiver` 地址。`mint(uint shares, address receiver)` 与它类似,只不过是以将铸造的金库份额作为参数。 @@ -448,6 +448,8 @@ contract ERC4626 is ERC20, IERC4626 { } ``` +当然,本文中的`ERC4626`合约仅是为了教学演示使用,在实际使用时,还需要考虑如`Inflation Attack`, `Rounding Direction`等问题。在生产中,建议使用`openzeppelin`的具体实现。 + ## `Remix`演示 **注意:** 以下运行示例使用了remix中第二个账户,即`0xAb8483F64d9C6d1EcF9b849Ae677dD3315835cb2`, 来部署合约, 调用合约方法. diff --git a/53_ERC20Permit/readme.md b/53_ERC20Permit/readme.md index f2eec7607..8f26af3e6 100644 --- a/53_ERC20Permit/readme.md +++ b/53_ERC20Permit/readme.md @@ -46,7 +46,7 @@ EIP-2612 提出了 ERC20Permit,扩展了 ERC20 标准,添加了一个 `permi - `spender` 不能是零地址。 - `deadline` 必须是未来的时间戳。 - - `v`,`r` 和 `s` 必须是 `owner` 对 EIP712 格式的函数参数的有效 `secp256k1` 签名。 + - `v`,`r` 和 `s` 必须是 `owner` 对 EIP712 格式的函数参数的有效 `keccak256` 签名。 - 签名必须使用 `owner` 当前的 nonce。 diff --git a/56_DEX/img/56-10.jpg b/56_DEX/img/56-10.jpg new file mode 100644 index 000000000..6b97924d3 Binary files /dev/null and b/56_DEX/img/56-10.jpg differ diff --git a/56_DEX/img/56-11.jpg b/56_DEX/img/56-11.jpg new file mode 100644 index 000000000..b2a989aca Binary files /dev/null and b/56_DEX/img/56-11.jpg differ diff --git a/56_DEX/img/56-2.jpg b/56_DEX/img/56-2.jpg new file mode 100644 index 000000000..244c0cbb0 Binary files /dev/null and b/56_DEX/img/56-2.jpg differ diff --git a/56_DEX/img/56-3.jpg b/56_DEX/img/56-3.jpg new file mode 100644 index 000000000..6fad30735 Binary files /dev/null and b/56_DEX/img/56-3.jpg differ diff --git a/56_DEX/img/56-4.jpg b/56_DEX/img/56-4.jpg new file mode 100644 index 000000000..d1129fc04 Binary files /dev/null and b/56_DEX/img/56-4.jpg differ diff --git a/56_DEX/img/56-5.jpg b/56_DEX/img/56-5.jpg new file mode 100644 index 000000000..f6f731527 Binary files /dev/null and b/56_DEX/img/56-5.jpg differ diff --git a/56_DEX/img/56-6.jpg b/56_DEX/img/56-6.jpg new file mode 100644 index 000000000..e30fce018 Binary files /dev/null and b/56_DEX/img/56-6.jpg differ diff --git a/56_DEX/img/56-7.jpg b/56_DEX/img/56-7.jpg new file mode 100644 index 000000000..593d3f32c Binary files /dev/null and b/56_DEX/img/56-7.jpg differ diff --git a/56_DEX/img/56-8.jpg b/56_DEX/img/56-8.jpg new file mode 100644 index 000000000..174967259 Binary files /dev/null and b/56_DEX/img/56-8.jpg differ diff --git a/56_DEX/img/56-9.jpg b/56_DEX/img/56-9.jpg new file mode 100644 index 000000000..8475035b1 Binary files /dev/null and b/56_DEX/img/56-9.jpg differ diff --git a/56_DEX/readme.md b/56_DEX/readme.md index 5c1b1a292..399d6a0dd 100644 --- a/56_DEX/readme.md +++ b/56_DEX/readme.md @@ -67,7 +67,7 @@ contract SimpleSwap is ERC20 { // 代币储备量 uint public reserve0; uint public reserve1; - + // 构造器,初始化代币地址 constructor(IERC20 _token0, IERC20 _token1) ERC20("SimpleSwap", "SS") { token0 = _token0; @@ -85,11 +85,11 @@ contract SimpleSwap is ERC20 { 首先,我们需要实现添加流动性的功能。当用户向代币池添加流动性时,合约要记录添加的LP份额。根据 Uniswap V2,LP份额如下计算: 1. 代币池被首次添加流动性时,LP份额 $\Delta{L}$ 由添加代币数量乘积的平方根决定: - + $$\Delta{L}=\sqrt{\Delta{x} *\Delta{y}}$$ -1. 非首次添加流动性时,LP份额由添加代币数量占池子代币储备量的比例决定(两个代币的比例取更小的那个): - +2. 非首次添加流动性时,LP份额由添加代币数量占池子代币储备量的比例决定(两个代币的比例取更小的那个): + $$\Delta{L}=L*\min{(\frac{\Delta{x}}{x}, \frac{\Delta{y}}{y})}$$ 因为 `SimpleSwap` 合约继承了 ERC20 代币标准,在计算好LP份额后,可以将份额以代币形式铸造给用户。 @@ -131,7 +131,7 @@ function addLiquidity(uint amount0Desired, uint amount1Desired) public returns(u // 给流动性提供者铸造LP代币,代表他们提供的流动性 _mint(msg.sender, liquidity); - + emit Mint(msg.sender, amount0Desired, amount1Desired); } ``` @@ -139,6 +139,7 @@ function addLiquidity(uint amount0Desired, uint amount1Desired) public returns(u 接下来,我们需要实现移除流动性的功能。当用户从池子中移除流动性 $\Delta{L}$ 时,合约要销毁LP份额代币,并按比例将代币返还给用户。返还代币的计算公式如下: $$\Delta{x}={\frac{\Delta{L}}{L} * x}$$ + $$\Delta{y}={\frac{\Delta{L}}{L} * y}$$ 下面的 `removeLiquidity()` 函数实现移除流动性的功能,主要步骤如下: @@ -149,7 +150,7 @@ $$\Delta{y}={\frac{\Delta{L}}{L} * y}$$ 4. 销毁LP份额。 5. 将相应的代币转账给用户。 6. 更新储备量。 -5. 释放 `Burn` 事件。 +7. 释放 `Burn` 事件。 ```solidity // 移除流动性,销毁LP,转出代币 @@ -228,7 +229,7 @@ function getAmountOut(uint amountIn, uint reserveIn, uint reserveOut) public pur function swap(uint amountIn, IERC20 tokenIn, uint amountOutMin) external returns (uint amountOut, IERC20 tokenOut){ require(amountIn > 0, 'INSUFFICIENT_OUTPUT_AMOUNT'); require(tokenIn == token0 || tokenIn == token1, 'INVALID_TOKEN'); - + uint balance0 = token0.balanceOf(address(this)); uint balance1 = token1.balanceOf(address(this)); @@ -278,7 +279,7 @@ contract SimpleSwap is ERC20 { // 代币储备量 uint public reserve0; uint public reserve1; - + // 事件 event Mint(address indexed sender, uint amount0, uint amount1); event Burn(address indexed sender, uint amount0, uint amount1); @@ -343,7 +344,7 @@ contract SimpleSwap is ERC20 { // 给流动性提供者铸造LP代币,代表他们提供的流动性 _mint(msg.sender, liquidity); - + emit Mint(msg.sender, amount0Desired, amount1Desired); } @@ -391,7 +392,7 @@ contract SimpleSwap is ERC20 { function swap(uint amountIn, IERC20 tokenIn, uint amountOutMin) external returns (uint amountOut, IERC20 tokenOut){ require(amountIn > 0, 'INSUFFICIENT_OUTPUT_AMOUNT'); require(tokenIn == token0 || tokenIn == token1, 'INVALID_TOKEN'); - + uint balance0 = token0.balanceOf(address(this)); uint balance1 = token1.balanceOf(address(this)); @@ -427,18 +428,32 @@ contract SimpleSwap is ERC20 { ## Remix 复现 1. 部署两个ERC20代币合约(token0 和 token1),并记录它们的合约地址。 + + ![创建ERC20token](img/56-2.jpg) 2. 部署 `SimpleSwap` 合约,并将上面的代币地址填入。 + + TOKEN地址传入token地址 3. 调用两个ERC20代币的`approve()`函数,分别给 `SimpleSwap` 合约授权 1000 单位代币。 + + token0授权token1授权 4. 调用 `SimpleSwap` 合约的 `addLiquidity()` 函数给交易所添加流动性,token0 和 token1 分别添加 100 单位。 + + 调用addLiquidityreserve 5. 调用 `SimpleSwap` 合约的 `balanceOf()` 函数查看用户的LP份额,这里应该为 100。($\sqrt{100*100}=100$) + + ![balanceOf](img/56-9.jpg) 6. 调用 `SimpleSwap` 合约的 `swap()` 函数进行代币交易,用 100 单位的 token0。 + + ![swap](img/56-10.jpg) 7. 调用 `SimpleSwap` 合约的 `reserve0` 和 `reserve1` 函数查看合约中的代币储备粮,应为 200 和 50。上一步我们利用 100 单位的 token0 交换了 50 单位的 token 1($\frac{100*100}{100+100}=50$)。 + + ![reserveAfter](img/56-11.jpg) ## 总结 diff --git a/Languages/en/02_ValueTypes_en/readme.md b/Languages/en/02_ValueTypes_en/readme.md index 1be98e2f6..ab8fb3307 100644 --- a/Languages/en/02_ValueTypes_en/readme.md +++ b/Languages/en/02_ValueTypes_en/readme.md @@ -63,9 +63,9 @@ Integers types in Solidity include signed integer `int` and unsigned integer `ui ```solidity // Integer - int public _int = -1; // integers including negative numbers - uint public _uint = 1; // non-negative numbers - uint256 public _number = 20220330; // 256-bit positive integers + int public _int = -1; // integers including negative integers + uint public _uint = 1; // unsigned integers + uint256 public _number = 20220330; // 256-bit unsigned integers ``` Commonly used integer operators include: @@ -79,7 +79,7 @@ Code: uint256 public _number1 = _number + 1; // +, -, *, / uint256 public _number2 = 2**2; // Exponent uint256 public _number3 = 7 % 2; // Modulo (Modulus) - bool public _numberbool = _number2 > _number3; // Great than + bool public _numberbool = _number2 > _number3; // Greater than ``` You can run the above code and check the values of each variable. diff --git a/Languages/en/14_Interface_en/readme.md b/Languages/en/14_Interface_en/readme.md index 525029ca6..1730031c4 100644 --- a/Languages/en/14_Interface_en/readme.md +++ b/Languages/en/14_Interface_en/readme.md @@ -53,7 +53,7 @@ other Dapps and smart contracts will know how to interact with it. Because it pr In addition, the interface is equivalent to the contract `ABI` (Application Binary Interface), and they can be converted to each other: compiling the interface contract will give you the contract `ABI`, -and [abi-to-sol tool](https://gnidan.github.io/ abi-to-sol/) will convert the `ABI` back to the interface contract. +and [abi-to-sol tool](https://gnidan.github.io/abi-to-sol/) will convert the `ABI` back to the interface contract. We take the `IERC721` contract, the interface for the `ERC721` token standard, as an example. It consists of 3 events and 9 functions, which all `ERC721` contracts need to implement. In the interface, each function ends with `;` instead of the function body `{ }`. Moreover, every function in the interface contract is by default `virtual`, so you do not need to label the function as `virtual` explicitly. @@ -86,8 +86,8 @@ interface IERC721 is IERC165 { ### IERC721 Event `IERC721` contains 3 events. -- `Transfer` event: emitted during transfer, records the sending address `from`, the receiving address `to`, and `tokenid`. -- `Approval` event: emitted during approval, records the token owner address `owner`, the approved address `approved`, and `tokenid`. +- `Transfer` event: emitted during transfer, records the sending address `from`, the receiving address `to`, and `tokenId`. +- `Approval` event: emitted during approval, records the token owner address `owner`, the approved address `approved`, and `tokenId`. - `ApprovalForAll` event: emitted during batch approval, records the owner address `owner` of batch approval, the approved address `operator`, and whether the approve is enabled or disabled `approved`. ### IERC721 Function diff --git a/Languages/en/20_SendETH_en/readme.md b/Languages/en/20_SendETH_en/readme.md index 0d1a99788..4d6c666ab 100644 --- a/Languages/en/20_SendETH_en/readme.md +++ b/Languages/en/20_SendETH_en/readme.md @@ -82,7 +82,7 @@ In the `ReceiveETH` contract, when we call `getBalance()`, we can see the balanc - Usage: `receiverAddress.send(value in Wei)`. - The `gas` limit of `send()` is `2300`, which is enough to make the transfer, but not if the receiving contract has a gas-consuming `fallback()` or `receive()`. -- If `send()` fails, the transaction will be `reverted`. +- If `send()` fails, the transaction will not be `reverted`. - The return value of `send()` is `bool`, which is the status of the transaction, you can choose to act on that. Sample Code: diff --git a/Languages/en/34_ERC721_en/readme.md b/Languages/en/34_ERC721_en/readme.md index c38b668f7..b3bf5232e 100644 --- a/Languages/en/34_ERC721_en/readme.md +++ b/Languages/en/34_ERC721_en/readme.md @@ -587,7 +587,7 @@ The calculation of **0x5b5e139f** is: IERC721Metadata.name.selector ^ IERC721Metadata.symbol.selector ^ IERC721Metadata.tokenURI.selector ``` -How does the ERC721.sol implemented by Solamte fulfil these features required by `ERC165`? +How does the ERC721.sol implemented by Solmate fulfil these features required by `ERC165`? ```solidity function supportsInterface(bytes4 interfaceId) public view virtual returns (bool) { diff --git a/Languages/en/35_DutchAuction_en/DutchAuction.sol b/Languages/en/35_DutchAuction_en/DutchAuction.sol index 104ef57ed..198d30a36 100644 --- a/Languages/en/35_DutchAuction_en/DutchAuction.sol +++ b/Languages/en/35_DutchAuction_en/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; // Total number of NFTs + uint256 public constant COLLECTION_SIZE = 10000; // Total number of NFTs uint256 public constant AUCTION_START_PRICE = 1 ether; // Starting price (highest price) uint256 public constant AUCTION_END_PRICE = 0.1 ether; // End price (lowest price/floor price) uint256 public constant AUCTION_TIME = 10 minutes; // Auction duration. Set to 10 minutes for testing convenience @@ -20,7 +20,7 @@ contract DutchAuction is Ownable, ERC721 { // Set auction start time: We declare the current block time as the start time in the constructor. // The project owner can also adjust the start time through the `setAuctionStartTime(uint32)` function. - constructor() ERC721("WTF Dutch Auctoin", "WTF Dutch Auctoin") { + constructor() Ownable(msg.sender) ERC721("WTF Dutch Auction", "WTF Dutch Auction") { auctionStartTime = block.timestamp; } @@ -46,7 +46,7 @@ contract DutchAuction is Ownable, ERC721 { "sale has not started yet" ); // checks if the start time of auction has been set and auction has started require( - totalSupply() + quantity <= COLLECTOIN_SIZE, + totalSupply() + quantity <= COLLECTION_SIZE, "not enough remaining reserved for auction to support desired mint amount" ); // checks if the number of NFTs has exceeded the limit diff --git a/Languages/en/35_DutchAuction_en/readme.md b/Languages/en/35_DutchAuction_en/readme.md index 7d3e70157..f9f38092b 100644 --- a/Languages/en/35_DutchAuction_en/readme.md +++ b/Languages/en/35_DutchAuction_en/readme.md @@ -62,7 +62,7 @@ There are a total of `9` state variables in the contract, of which `6` are relat - `auctionStartTime`: Starting time of the auction (blockchain timestamp, `block.timestamp`). ```solidity - uint256 public constant COLLECTOIN_SIZE = 10000; // Total number of NFTs + uint256 public constant COLLECTION_SIZE = 10000; // Total number of NFTs uint256 public constant AUCTION_START_PRICE = 1 ether; // Starting price (highest price) uint256 public constant AUCTION_END_PRICE = 0.1 ether; // End price (lowest price/floor price) uint256 public constant AUCTION_TIME = 10 minutes; // Auction duration. Set to 10 minutes for testing convenience @@ -126,7 +126,7 @@ First, the function checks if the auction has started or if the number of `NFTs` "sale has not started yet" ); // checks if the start time of auction has been set and auction has started require( - totalSupply() + quantity <= COLLECTOIN_SIZE, + totalSupply() + quantity <= COLLECTION_SIZE, "not enough remaining reserved for auction to support desired mint amount" ); // checks if the number of NFTs has exceeded the limit diff --git a/Languages/en/40_ERC1155_en/ERC1155.sol b/Languages/en/40_ERC1155_en/ERC1155.sol index 3dbebad03..ba2ac5bf2 100644 --- a/Languages/en/40_ERC1155_en/ERC1155.sol +++ b/Languages/en/40_ERC1155_en/ERC1155.sol @@ -14,7 +14,7 @@ import "../34_ERC721_en/IERC165.sol"; */ contract ERC1155 is IERC165, IERC1155, IERC1155MetadataURI { using Address for address; // use the Address library, isContract to determine whether the address is a contract - using Strings for uint256; // use the String library + using Strings for uint256; // use the Strings library // Token name string public name; // Token code name diff --git a/Languages/en/40_ERC1155_en/readme.md b/Languages/en/40_ERC1155_en/readme.md index 1d20b4a3d..45c411f63 100644 --- a/Languages/en/40_ERC1155_en/readme.md +++ b/Languages/en/40_ERC1155_en/readme.md @@ -267,7 +267,7 @@ import "../34_ERC721_en/IERC165.sol"; */ contract ERC1155 is IERC165, IERC1155, IERC1155MetadataURI { using Address for address; // use the Address library, isContract to determine whether the address is a contract - using Strings for uint256; // use the String library + using Strings for uint256; // use the Strings library // Token name string public name; // Token code name diff --git a/Languages/en/50_MultisigWallet_en/readme.md b/Languages/en/50_MultisigWallet_en/readme.md index db76bf5dc..196462d89 100644 --- a/Languages/en/50_MultisigWallet_en/readme.md +++ b/Languages/en/50_MultisigWallet_en/readme.md @@ -290,7 +290,7 @@ Transaction hash: 0xb43ad6901230f2c59c3f7ef027c9a372f199661c61beeec49ef5a774231f 多签地址2的签名: 0xbe2e0e6de5574b7f65cad1b7062be95e7d73fe37dd8e888cef5eb12e964ddc597395fa48df1219e7f74f48d86957f545d0fbce4eee1adfbaff6c267046ade0d81c - 讲两个签名拼接到一起,得到打包签名: 0x014db45aa753fefeca3f99c2cb38435977ebb954f779c2b6af6f6365ba4188df542031ace9bdc53c655ad2d4794667ec2495196da94204c56b1293d0fbfacbb11cbe2e0e6de5574b7f65cad1b7062be95e7d73fe37dd8e888cef5eb12e964ddc597395fa48df1219e7f74f48d86957f545d0fbce4eee1adfbaff6c267046ade0d81c + 将两个签名拼接到一起,得到打包签名: 0x014db45aa753fefeca3f99c2cb38435977ebb954f779c2b6af6f6365ba4188df542031ace9bdc53c655ad2d4794667ec2495196da94204c56b1293d0fbfacbb11cbe2e0e6de5574b7f65cad1b7062be95e7d73fe37dd8e888cef5eb12e964ddc597395fa48df1219e7f74f48d86957f545d0fbce4eee1adfbaff6c267046ade0d81c ``` ![Signature](./img/50-5-1.png) diff --git a/Languages/pt-br/35_DutchAuction/readme.md b/Languages/pt-br/35_DutchAuction/readme.md index 10aa969fc..d0433e839 100644 --- a/Languages/pt-br/35_DutchAuction/readme.md +++ b/Languages/pt-br/35_DutchAuction/readme.md @@ -43,7 +43,7 @@ contract DutchAuction is Ownable, ERC721 { Há um total de 9 variáveis de estado no contrato, sendo 6 relacionadas ao leilão. Elas são: -- `COLLECTOIN_SIZE`: total de NFTs. +- `COLLECTION_SIZE`: total de NFTs. - `AUCTION_START_PRICE`: preço inicial e mais alto do leilão holandês. - `AUCTION_END_PRICE`: preço final e mais baixo do leilão holandês. - `AUCTION_TIME`: duração do leilão. @@ -51,7 +51,7 @@ Há um total de 9 variáveis de estado no contrato, sendo 6 relacionadas ao leil - `auctionStartTime`: timestamp de início do leilão (utilizando `block.timestamp`). ```solidity - uint256 public constant COLLECTOIN_SIZE = 10000; // Total de NFTs + uint256 public constant COLLECTION_SIZE = 10000; // Total de NFTs uint256 public constant AUCTION_START_PRICE = 1 ether; // Preço inicial (mais alto) uint256 public constant AUCTION_END_PRICE = 0.1 ether; // Preço final (mais baixo) uint256 public constant AUCTION_TIME = 10 minutes; // Duração do leilão, apenas para fins de teste diff --git a/README.md b/README.md index e2d0269c0..76748f05f 100644 --- a/README.md +++ b/README.md @@ -271,6 +271,8 @@ **第7讲:Foundry,以Solidity为中心的开发工具包** 【[代码](https://github.com/AmazingAng/WTF-Solidity/blob/main/Topics/Tools/TOOL07_Foundry)】【[文章](https://github.com/AmazingAng/WTF-Solidity/blob/main/Topics/Tools/TOOL07_Foundry/readme.md)】 +**第8讲:ZAN,节点服务和合约审计等Web3技术服务** 【[文章](https://github.com/AmazingAng/WTFSolidity/blob/main/Topics/Tools/TOOL08_ZAN/readme.md)】 + ### 链上威胁分析 **第1讲:工具篇** 【[文章](https://github.com/AmazingAng/WTF-Solidity/tree/main/Topics/Onchain_debug//01_tools/)】 | 【[English](https://github.com/AmazingAng/WTF-Solidity/tree/main/Topics/Onchain_debug/01_tools/en/)】 diff --git a/S01_ReentrancyAttack/readme.md b/S01_ReentrancyAttack/readme.md index 3f688cbc7..c1fd0cdd0 100644 --- a/S01_ReentrancyAttack/readme.md +++ b/S01_ReentrancyAttack/readme.md @@ -158,6 +158,8 @@ contract Attack { 4. 调用`Bank`合约的`getBalance()`函数,发现余额已被提空。 5. 调用`Attack`合约的`getBalance()`函数,可以看到余额变为`21 ETH`,重入攻击成功。 +当然,不仅仅`ETH`转账会触发重入攻击,`ERC721`和`ERC1155`的`safeTransfer()`和`safeTransferFrom()`安全转账函数,还有`ERC777`的`callback`函数,都可能会引发重入攻击。所以这更多的是一个宏观上的设计问题,而不仅仅局限于ETH转账本身。 + ## 预防办法 目前主要有两种办法来预防可能的重入攻击漏洞: 检查-影响-交互模式(checks-effect-interaction)和重入锁。 diff --git a/S02_SelectorClash/SelectorClash.sol b/S02_SelectorClash/SelectorClash.sol index 59d2aad6c..7c400261b 100644 --- a/S02_SelectorClash/SelectorClash.sol +++ b/S02_SelectorClash/SelectorClash.sol @@ -10,8 +10,8 @@ contract SelectorClash { solved = true; } - function executeCrossChainTx(bytes memory _method, bytes memory _bytes) public returns(bool success){ - (success, ) = address(this).call(abi.encodePacked(bytes4(keccak256(abi.encodePacked(_method, "(bytes,bytes,uint64)"))), abi.encode(_bytes))); + function executeCrossChainTx(bytes memory _method, bytes memory _bytes, bytes memory _bytes1, uint64 _num) public returns(bool success){ + (success, ) = address(this).call(abi.encodePacked(bytes4(keccak256(abi.encodePacked(_method, "(bytes,bytes,uint64)"))), abi.encode(_bytes, _bytes1, _num))); } function secretSlector() external pure returns(bytes4){ diff --git a/S05_Overflow/readme.md b/S05_Overflow/readme.md index 134ad2398..a172078be 100644 --- a/S05_Overflow/readme.md +++ b/S05_Overflow/readme.md @@ -75,7 +75,7 @@ contract Token { ## 预防办法 -1. Solidity `0.8.0` 之前的版本,在合约中引用 [Safemath 库](https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/utils/math/SafeMath.sol),在整型溢出时报错。 +1. Solidity `0.8.0` 之前的版本,在合约中引用 [Safemath 库](https://github.com/OpenZeppelin/openzeppelin-contracts/blob/release-v4.9/contracts/utils/math/SafeMath.sol),在整型溢出时报错。 2. Solidity `0.8.0` 之后的版本内置了 `Safemath`,因此几乎不存在这类问题。开发者有时会为了节省gas使用 `unchecked` 关键字在代码块中临时关闭整型溢出检测,这时要确保不存在整型溢出漏洞。 diff --git a/S06_SignatureReplay/readme.md b/S06_SignatureReplay/readme.md index e1fc0d77e..f989daed2 100644 --- a/S06_SignatureReplay/readme.md +++ b/S06_SignatureReplay/readme.md @@ -157,10 +157,21 @@ contract SigReplay is ERC20 { } ``` +3. 对于由用户输入`signature`的场景,需要检验`signature`的长度,确保其长度为`65bytes`,否则也会产生签名重放问题。 + + ```solidity + function mint(address to, uint amount, bytes memory signature) public { + require(signature.length == 65, "Invalid signature length"); + ... + } + ``` + ## 总结 -这一讲,我们介绍了智能合约中的签名重放漏洞,并介绍了两个预防方法: +这一讲,我们介绍了智能合约中的签名重放漏洞,并介绍了三个预防方法: 1. 将使用过的签名记录下来,防止二次使用。 -2. 将 `nonce` 和 `chainid` 包含到签名消息中。 \ No newline at end of file +2. 将 `nonce` 和 `chainid` 包含到签名消息中。 + +3. 对于由用户输入`signature`的场景,需要检验`signature`的长度,确保其长度为`65bytes`,否则也会产生签名重放问题。 diff --git a/S09_DoS/readme.md b/S09_DoS/readme.md index f0308bb13..e8d210bb8 100644 --- a/S09_DoS/readme.md +++ b/S09_DoS/readme.md @@ -117,7 +117,7 @@ contract Attack { 2. 合约不会出乎意料的自毁。 3. 合约不会进入无限循环。 4. `require` 和 `assert` 的参数设定正确。 -5. 退款时,让用户从合约自行领取(push),而非批量发送给用户(pull)。 +5. 退款时,让用户从合约自行领取(pull),而非批量发送给用户(push)。 6. 确保回调函数不会影响正常合约运行。 7. 确保当合约的参与者(例如 `owner`)永远缺席时,合约的主要业务仍能顺利运行。 diff --git a/S10_Honeypot/readme.md b/S10_Honeypot/readme.md index e53eaa6bf..52ce7ead1 100644 --- a/S10_Honeypot/readme.md +++ b/S10_Honeypot/readme.md @@ -100,7 +100,7 @@ contract HoneyPot is ERC20, Ownable { 2. 调用 `mint()` 函数,给自己铸造 `100000` 枚貔貅币。 ![](./img/S10-3.png) -3. 进入 [uniswap](https://app.uniswap.org/#/add/v2/ETH) 交易所,为貔貅币创造流动性(v2),提供 `10000`貔貅币。和 `0.1` ETH。 +3. 进入 [uniswap](https://app.uniswap.org/#/add/v2/ETH) 交易所,为貔貅币创造流动性(v2),提供 `10000`貔貅币和 `0.1` ETH。 ![](./img/S10-4.png) 4. 出售 `100` 貔貅币,能够操作成功。 @@ -132,7 +132,7 @@ contract HoneyPot is ERC20, Ownable { 4. 仔细检查项目的官网和社交媒体。 -5. 只投资你了解的项目,做好研究(DYOR。 +5. 只投资你了解的项目,做好研究(DYOR)。 6. 使用tenderly、phalcon分叉模拟卖出貔貅,如果失败则确定是貔貅代币。 diff --git a/S13_UncheckedCall/readme.md b/S13_UncheckedCall/readme.md index c9f3de97a..5eab30c8a 100644 --- a/S13_UncheckedCall/readme.md +++ b/S13_UncheckedCall/readme.md @@ -125,4 +125,4 @@ contract Attack { ## 总结 -我们将介绍未检查低级调用的漏洞及其预防方法。以太坊的低级调用(call, delegatecall, staticcall, send)在失败时会返回一个布尔值 false,但不会让整个交易回滚。如果开发者没有对它进行检查的话,则会发生意外。 \ No newline at end of file +这一讲,我们介绍了未检查低级调用的漏洞及其预防方法。以太坊的低级调用(call, delegatecall, staticcall, send)在失败时会返回一个布尔值 false,但不会让整个交易回滚。如果开发者没有对它进行检查的话,则会发生意外。 \ No newline at end of file diff --git a/S15_OracleManipulation/readme.md b/S15_OracleManipulation/readme.md index fc5161ff6..8db1b5f59 100644 --- a/S15_OracleManipulation/readme.md +++ b/S15_OracleManipulation/readme.md @@ -185,7 +185,7 @@ contract OracleTest is Test { // 给自己账户 1000000 BUSD uint busdAmount = 1_000_000 * 10e18; deal(BUSD, alice, busdAmount); - // 2. 用busd买weth,推高顺时价格 + // 2. 用busd买weth,推高瞬时价格 vm.prank(alice); busd.transfer(address(this), busdAmount); swapBUSDtoWETH(busdAmount, 1); diff --git a/S15_OracleManipulation/test/Oracle.t.sol b/S15_OracleManipulation/test/Oracle.t.sol index 04cc14717..5764d5913 100644 --- a/S15_OracleManipulation/test/Oracle.t.sol +++ b/S15_OracleManipulation/test/Oracle.t.sol @@ -32,7 +32,7 @@ contract OracleTest is Test { // 给自己账户 1000000 BUSD uint busdAmount = 1_000_000 * 10e18; deal(BUSD, alice, busdAmount); - // 2. 用busd买weth,推高顺时价格 + // 2. 用busd买weth,推高瞬时价格 vm.prank(alice); busd.transfer(address(this), busdAmount); swapBUSDtoWETH(busdAmount, 1); diff --git "a/Topics/StudyNotes/Notes_34_ERC721_ShuxunOo/ERC721\345\256\236\344\276\213\345\210\206\346\236\220\346\200\273\347\273\223_ShuxunOo/ERC721 \347\273\274\350\277\260.md" "b/Topics/StudyNotes/Notes_34_ERC721_ShuxunOo/ERC721\345\256\236\344\276\213\345\210\206\346\236\220\346\200\273\347\273\223_ShuxunOo/ERC721 \347\273\274\350\277\260.md" index 526ec2dcb..751d92008 100644 --- "a/Topics/StudyNotes/Notes_34_ERC721_ShuxunOo/ERC721\345\256\236\344\276\213\345\210\206\346\236\220\346\200\273\347\273\223_ShuxunOo/ERC721 \347\273\274\350\277\260.md" +++ "b/Topics/StudyNotes/Notes_34_ERC721_ShuxunOo/ERC721\345\256\236\344\276\213\345\210\206\346\236\220\346\200\273\347\273\223_ShuxunOo/ERC721 \347\273\274\350\277\260.md" @@ -152,7 +152,7 @@ interface ERC165 { /// @notice 查询合约是否实现了某个接口 /// @param interfaceID 在 ERC-165 中指定的用于标识接口的ID, as specified in ERC-165 /// @dev 接口标识在 ERC-165 中指定。 这个功能使用少于 30,000 个气体 - /// @return 如果合约实现了`interfaceID`不为0xffffffff的接口 则返回返回`true 否则返回 `false` + /// @return 如果合约实现了`interfaceID`不为0xffffffff的接口 则返回`true 否则返回 `false` function supportsInterface(bytes4 interfaceID) external view returns (bool); } ``` diff --git a/Topics/Tools/TOOL06_Hardhat/readme.md b/Topics/Tools/TOOL06_Hardhat/readme.md index a316b7869..ab2cdd79f 100644 --- a/Topics/Tools/TOOL06_Hardhat/readme.md +++ b/Topics/Tools/TOOL06_Hardhat/readme.md @@ -75,7 +75,7 @@ module.exports = { 新建`contracts`合约目录,并添加第31章节的ERC20合约。 ### 编写合约 -这里的合约直接使用[WTF Solidity第31讲](https://github.com/AmazingAng/WTF-Solidity/blob/main/31_ERC20/readme.md]的ERC20合约 +这里的合约直接使用[WTF Solidity第31讲](https://github.com/AmazingAng/WTF-Solidity/blob/main/31_ERC20/readme.md)的ERC20合约 ```js // SPDX-License-Identifier: MIT diff --git a/Topics/Tools/TOOL07_Foundry/readme.md b/Topics/Tools/TOOL07_Foundry/readme.md index bc11fc5b0..58160bfd0 100644 --- a/Topics/Tools/TOOL07_Foundry/readme.md +++ b/Topics/Tools/TOOL07_Foundry/readme.md @@ -336,8 +336,7 @@ cast block-number --rpc-url=$RPC_MAIN ``` 15769241 ``` - -> 将环境变量的`ETH_RPC_URL`设置为 `--rpc-url` 你就不需要在每个命令行后面增加 `--rpc-url=$RPC_MAIN` 我这里直接设置为主网 +> 将环境变量的ETH_RPC_URL设置为 --rpc-url 你就不需要在每个命令行后面增加 --rpc-url=$RPC_MAIN 我这里直接设置为主网 ### 查询区块信息 @@ -596,9 +595,9 @@ cast etherscan-source 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2 --etherscan-api cast etherscan-source $WETH -d ~/Downloads ``` -### 调用合约 +### 调用合约(读数据) -调用 WETH合约的`balanceOf`方法,查看`0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2`账号的余额 +`cast call` 用于从区块链上读取数据(调用只读函数),该操作不会改变区块链状态,也不需要支付 Gas 费用。下面我们调用 `WETH` 合约的`balanceOf`方法,查看 `0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2` 账号的余额 ```shell #cast call [OPTIONS] [TO] [SIG] [ARGS]... [COMMAND] --rpc-url=$RPC @@ -617,6 +616,18 @@ cast call $WETH "balanceOf(address)(uint256)" 0xC02aaA39b223FE8D0A0e5C4F27eAD908 ``` +### 调用合约(写数据) + +`cast send` 用于发送交易并修改区块链状态(调用改变状态的函数),该操作会消耗 Gas 费用,需要签名并发送交易。 + +```shell +#cast send [OPTIONS] [TO] [SIG] [ARGS]... [COMMAND] --rpc-url=$RPC + +cast send 0x... "deposit(address,uint256)" 0x... 1 --rpc-url=$RPC + +``` + + ### 解析ABI 可以根据ABI反向解析出solidity代码 @@ -645,7 +656,13 @@ cast --from-rlp ## Tips ### 设置ETH_RPC_URL -将环境变量的`ETH_RPC_URL`设置为 `--rpc-url` 你就不需要在每个命令行后面增加 `--rpc-url=$RPC_MAIN` 我这里直接设置为主网 +将环境变量的`ETH_RPC_URL`设置为 `--rpc-url`的值,这样你就不需要在每个命令行后面增加 `--rpc-url=$RPC_MAIN`,我这里直接设置为主网,如下: +``` shell +export ETH_RPC_URL=your_rpc_url +source ~/.bashrc # 如果你使用的是 Bash +source ~/.zshrc # 如果你使用的是 Zsh +cast block-number # 不用再配置 --rpc-url 参数 +``` ### 设置ETHERSCAN_API_KEY 设置`ETHERSCAN_API_KEY`环境变量可以直接代替 `--etherscan-api-key` @@ -887,7 +904,7 @@ contract Helper { 运行测试: -因为本地没有dai的部署合约,所以我们直接fork主网, -vvv可以现实console2.log,-w表示watch模式。 +因为本地没有dai的部署合约,所以我们直接fork主网, -vvv可以显示console2.log,-w表示watch模式。 ```shell forge test -vvv --fork-url=$ETH_RPC_URL -w @@ -1114,7 +1131,7 @@ forge script script/Counter.s.sol -vvvv --rpc-url=http://127.0.0.1:8545 正式部署: ``` -forge script script/Counter.s.sol -vvvv --rpc-url=http://127.0.0.1:8545 --broadcast --private-key=privete_key +forge script script/Counter.s.sol -vvvv --rpc-url=$RPC_MAIN --broadcast --private-key=privete_key ``` 部署完成之后会多一个broadcast文件夹,查看该文件夹有`run-latest.json`可以看到部署的相应信息。 @@ -1128,6 +1145,18 @@ forge script script/Counter.s.sol -vvvv --rpc-url=http://127.0.0.1:8545 --broadc uint256 mainnet = vm.createFork(rpc); ``` +## 命令行部署合约 + +部署并开源合约,其中 `--etherscan-api-key` 为以太坊浏览器(或其他 EVM 浏览器)中申请的 API key,`--verify` 为部署后开源合约。 +```solidity +# forge create --rpc-url --private-key --etherscan-api-key --verify src/YourContract.sol:YourContract --constructor-args + forge create --rpc-url $RPC_MAIN \ + --private-key privete_key \ + --etherscan-api-key xxxx \ + --verify \ + src/Counter.sol:Counter +``` + ## Tips: ```shell @@ -1160,4 +1189,4 @@ forge run --debug ## 参考 [使用 foundry 框架加速智能合约开发](https://www.youtube.com/watch?v=EXYeltwvftw) [cast Commands - Foundry Book](https://book.getfoundry.sh/reference/cast/) -[https://twitter.com/wp__lai](https://twitter.com/wp__lai) \ No newline at end of file +[https://twitter.com/wp__lai](https://twitter.com/wp__lai) diff --git a/Topics/Tools/TOOL08_ZAN/img/config-vscode.png b/Topics/Tools/TOOL08_ZAN/img/config-vscode.png new file mode 100644 index 000000000..0cf3c6ede Binary files /dev/null and b/Topics/Tools/TOOL08_ZAN/img/config-vscode.png differ diff --git a/Topics/Tools/TOOL08_ZAN/img/kyt-demo.png b/Topics/Tools/TOOL08_ZAN/img/kyt-demo.png new file mode 100644 index 000000000..7cf8542c3 Binary files /dev/null and b/Topics/Tools/TOOL08_ZAN/img/kyt-demo.png differ diff --git a/Topics/Tools/TOOL08_ZAN/img/kyt-key.png b/Topics/Tools/TOOL08_ZAN/img/kyt-key.png new file mode 100644 index 000000000..dbfa34a6b Binary files /dev/null and b/Topics/Tools/TOOL08_ZAN/img/kyt-key.png differ diff --git a/Topics/Tools/TOOL08_ZAN/img/kyt-submit.png b/Topics/Tools/TOOL08_ZAN/img/kyt-submit.png new file mode 100644 index 000000000..255b33282 Binary files /dev/null and b/Topics/Tools/TOOL08_ZAN/img/kyt-submit.png differ diff --git a/Topics/Tools/TOOL08_ZAN/img/scr-new.png b/Topics/Tools/TOOL08_ZAN/img/scr-new.png new file mode 100644 index 000000000..7a664eb79 Binary files /dev/null and b/Topics/Tools/TOOL08_ZAN/img/scr-new.png differ diff --git a/Topics/Tools/TOOL08_ZAN/img/scr-pdf.png b/Topics/Tools/TOOL08_ZAN/img/scr-pdf.png new file mode 100644 index 000000000..216870235 Binary files /dev/null and b/Topics/Tools/TOOL08_ZAN/img/scr-pdf.png differ diff --git a/Topics/Tools/TOOL08_ZAN/img/scr-report.png b/Topics/Tools/TOOL08_ZAN/img/scr-report.png new file mode 100644 index 000000000..8de947fd7 Binary files /dev/null and b/Topics/Tools/TOOL08_ZAN/img/scr-report.png differ diff --git a/Topics/Tools/TOOL08_ZAN/img/scr-wait.png b/Topics/Tools/TOOL08_ZAN/img/scr-wait.png new file mode 100644 index 000000000..59a47c08d Binary files /dev/null and b/Topics/Tools/TOOL08_ZAN/img/scr-wait.png differ diff --git a/Topics/Tools/TOOL08_ZAN/img/service-setting.png b/Topics/Tools/TOOL08_ZAN/img/service-setting.png new file mode 100644 index 000000000..45319b90c Binary files /dev/null and b/Topics/Tools/TOOL08_ZAN/img/service-setting.png differ diff --git a/Topics/Tools/TOOL08_ZAN/img/vscode-key.png b/Topics/Tools/TOOL08_ZAN/img/vscode-key.png new file mode 100644 index 000000000..e3633929c Binary files /dev/null and b/Topics/Tools/TOOL08_ZAN/img/vscode-key.png differ diff --git a/Topics/Tools/TOOL08_ZAN/img/zan-service.png b/Topics/Tools/TOOL08_ZAN/img/zan-service.png new file mode 100644 index 000000000..b9296f290 Binary files /dev/null and b/Topics/Tools/TOOL08_ZAN/img/zan-service.png differ diff --git a/Topics/Tools/TOOL08_ZAN/readme.md b/Topics/Tools/TOOL08_ZAN/readme.md new file mode 100644 index 000000000..4af6e42dd --- /dev/null +++ b/Topics/Tools/TOOL08_ZAN/readme.md @@ -0,0 +1,182 @@ +# WTF Solidity 极简入门-工具篇 8:ZAN,节点服务和合约审计等 Web3 技术服务 + +我最近在重新学 Solidity,巩固一下细节,也写一个“WTF Solidity 极简入门”,供小白们使用(编程大佬可以另找教程),每周更新 1-3 讲。 + +推特:[@0xAA_Science](https://twitter.com/0xAA_Science)|[@WTFAcademy\_](https://twitter.com/WTFAcademy_) + +社区:[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/WTF-Solidity) + +--- + +## ZAN 是什么 + +[ZAN](https://zan.top?chInfo=wtf) 是一家 Web3 技术服务提供商,是蚂蚁集团下的一个技术品牌。它提供多种技术服务,有类似 Infura 和 Alchemy 提供的节点服务,包括各种链的 JSON-RPC 支持,以及其他高级的链 API。还有安全相关的合约审计、链上数据分析、KYC 等服务。另外还提供 toB 的解决方案,包括 RWA 资产的链上发行、零知识证明加速以及提供安全和可定制的区块链等。 + +接下来会针对开发者常用的节点服务、合约审计和链上数据分析(KNOW YOUR TRANSACTION)功能做介绍。在学习之前你可以先访问 [https://zan.top](https://zan.top?chInfo=wtf) 并完成注册和登录。 + +## 节点服务 + +### 创建 API Key + +之后进入到节点服务的控制台 [https://zan.top/service/apikeys](https://zan.top/service/apikeys?chInfo=wtf) 创建一个 Key,每个 Key 都有默认的免费额度,对于小项目来说够用了。 + +创建成功后你会看到如下的页面: + +![](./img/zan-service.png) + +接下来你就可以直接点击网站上的 Copy 按钮复制获取 API Key 了,但是大部分情况我们都是直接复制你需要的链的 RPC 地址,然后在你的 DApp 中使用。 + +### 使用 RPC 地址 + +不同的区块链通常都会有自己的基础的 JSON-RPC 的接口规范,比如以太坊的 JSON-RPC 接口规范,你可以在 [https://ethereum.org/en/developers/docs/apis/json-rpc/](https://ethereum.org/en/developers/docs/apis/json-rpc/) 获取。 + +ZAN 提供的接口自然也遵循这样的规范,比如你可以通过访问 `https://api.zan.top/node/v1/eth/mainnet/{YourZANApiKey}` 来访问以太坊上的数据。当然,我们通常可以直接使用链官方或者生态提供的 SDK 来调用接口。下面我们会介绍如何使用以太坊常用的一些 SDK 来调用 ZAN 提供的节点服务。 + +需要注意上面代码中的 `YourZANApiKey` 需要替换成你自己的 Key。另外在实际的项目中,为了避免你的 Key 被滥用,建议你将 Key 放到后端服务中,然后通过后端服务来调用节点服务,或者在 ZAN 的控制台中设置域名白名单来降低被滥用的风险。 + +#### 使用 ethers + +在 ethers.js 中,我们可以利用 ZAN 提供的 API Key 来创建 JsonRpcProvider,与链上交互。 + +```javascript +import { ethers } from "ethers"; + +const provider = new ethers.providers.JsonRpcProvider( + "https://api.zan.top/node/v1/eth/mainnet/{YourZANApiKey}" +); + +provider.getBlockNumber().then((blockNumber) => { + console.log("Current block number: " + blockNumber); +}); +``` + +如上所示,我们可以通过 ethers.js 提供的 JsonRpcProvider 来访问 ZAN 提供的节点服务,获取当前区块的高度。更多的 ethers.js 的使用方法可以参考 ethers.js 官方文档:[ethers V5](https://docs.ethers.org/v5/api/providers/jsonrpc-provider/) | [ethers V6](https://docs.ethers.org/v6/api/providers/jsonrpc/#JsonRpcProvider)。 + +#### 使用 web3.js + +在 web3.js 中,我们可以利用 ZAN 提供的 API Key 来创建 Web3Provider,与链上交互。 + +```javascript +import Web3 from "web3"; + +const web3 = new Web3( + "https://api.zan.top/node/v1/eth/mainnet/{YourZANApiKey}" +); +web3.eth.getBlockNumber().then((blockNumber) => { + console.log("Current block number: " + blockNumber); +}); +``` + +如上所示,我们可以通过 web3.js 提供的 Web3Provider 来访问 ZAN 提供的节点服务,获取当前区块的高度。更多的 web3.js 的使用方法可以参考 [web3.js 官方文档](https://web3js.readthedocs.io/) + +#### 使用 wagmi + +在 ZAN 的控制台中选择以太坊主网的节点服务地址复制,复制后的地址添加到 wagmi 的 `http()` 方法中,如下: + +```jsx +import { createConfig, http, WagmiProvider } from "wagmi"; +import { mainnet } from "wagmi/chains"; + +const config = createConfig({ + chains: [mainnet], + transports: { + [mainnet.id]: http( + "https://api.zan.top/node/v1/eth/mainnet/{YourZANApiKey}" + ), + }, +}); + +export default function App() { + return {/** ... */}; +} +``` + +如上,在 `WagmiProvider` 包裹的组件中你就可以使用 wagmi 提供的各种 hook 来访问以太坊的数据了。 + +### 安全设置 + +如果你在生产环境中使用,为了避免你的 API Key 被滥用或者攻击,你可以在 ZAN 的控制台中设置域名白名单,这样只有在白名单中的域名才能使用你的 API Key。 + +![setting](./img/service-setting.png) + +还有其他设置你也可以在 ZAN 的控制台中找到,比如你可以设置每个 Key 的访问频率限制,这样可以避免你的 Key 被滥用。也可以查看每个 Key 的使用情况,设置团队共同管理 Key 等。 + +### 测试网代币水龙头 + +在开发中,我们经常需要获取测试网的代币,你可以通过水龙头获取测试代币,ZAN 也提供了测试网代币的水龙头服务,你可以在 [https://zan.top/faucet](https://zan.top/faucet?chInfo=wtf) 中获取测试代币。 + +## 合约安全审计 + +合约安全审计是一个非常重要的环节,尤其是在 DeFi 项目中,合约的安全性直接关系到用户的资金安全。ZAN 提供了合约安全审计服务,你可以在 [https://zan.top/home/contract-review](https://zan.top/home/contract-review) 中提交你的合约进行审计。ZAN 提供了免费版和专家版,专家版是有人工介入,需要收费。我们以免费版为例,说明如何在 ZAN 中提交合约审计任务。你有两种方法,一种是在网站上提交,另外一种是通过 VSCode 插件提交。 + +### 通过 ZAN 网站提交 + +你可以在 [https://zan.top/review/reports/apply](https://zan.top/review/reports/apply?chInfo=wtf) 提交一个合约审计任务: + +![create](./img/scr-new.png) + +有多种提交方式,提交后会进入到自动化的程序进行审计,如下图所示,你需要等待几分钟。 + +![waiting](./img/scr-wait.png) + +需要注意的是,这种方式是自动化的审计,只能检测一些常见的漏洞,并无法保障合约的逻辑正确性,如果你的合约比较复杂,还是需要人工介入审计的。 + +### 通过 VSCode 插件提交 + +ZAN 提供了一个 [VSCode 插件](https://marketplace.visualstudio.com/items?itemName=zantop.zan-smart-contract-review),你可以在你的 VSCode 中安装该插件,直接在 VSCode 中提交合约审计任务。 + +首先方案插件地址 [https://marketplace.visualstudio.com/items?itemName=zantop.zan-smart-contract-review](https://marketplace.visualstudio.com/items?itemName=zantop.zan-smart-contract-review) 点击 Install 唤起 VSCode 安装插件。 + +安装成功后左侧会有一个 ZAN 的图标,点击图标后可以看到一个 `Config Access Key` 的按钮。 + +![config-vscode](./img/config-vscode.png) + +点击按钮后配置你的 Access Key,Key 需要在 ZAN 的合约审计控制台中获取: + +![getkey](./img/vscode-key.png) + +获取 Key 后填入 VSCode 插件配置中,然后你就可以在 VSCode 中提交合约审计任务了。 + +### 合约报告查看 + +合约分析完成后你可以在网站上查看报告: + +![src-report](./img/scr-report.png) + +比如上图是对一个简单的 ERC721 的[合约](https://github.com/ourmetaverse/our-metaverse-contract)的分析结果,你可以通过[https://zan.top/review/reports/public/b82c9992-bfce-4986-baff-5bb4e76e1eb9](https://zan.top/review/reports/public/b82c9992-bfce-4986-baff-5bb4e76e1eb9?chInfo=wtf)查看报告。 + +对于专家服务,会有多轮 AI + 专家人工审计后得到一个 PDF 报告,你可以访问[该链接](https://mdn.alipayobjects.com/huamei_hsbbrh/afts/file/A*tl-PR5pIIt4AAAAAAAAAAAAADiOMAQ/hebao_v3_20240313.pdf) 查看一个示例。 + +![pdf](./img/scr-pdf.png) + +## 链上数据分析(KNOW YOUR TRANSACTION) + +区块链上的数据都是公开透明的,然而要在浩瀚的链上数据中分析问题,查找线索也不是那么容易的事情。ZAN 提供了链上数据分析服务,你可以在 [https://zan.top/kyt/controller](https://zan.top/kyt/controller?chInfo=wtf) 中提交你的地址,查看交易的详细信息。 + +![kyt](./img/kyt-submit.png) + +提交的内容可以是一个地址,也可以是一笔交易,如下图所示,你可以看到资金的流动情况,还会标记出有风险的地址: + +![kyt-demo](./img/kyt-demo.png) + +可以用于资金追踪、巨鲸监控、反洗钱等等服务。对于专业用户,你可以使用 API 来调用,如下图你可以在控制台中获取你的 API Key: + +![kyt-key](./img/kyt-key.png) + +```sh +## Replace {apiKey} with the API key you obtained from the ZAN dashboard. +## You can also replace the "eth" and "mainnet" with any other supported networks. +curl https://api.zan.top/kyt/v1/score \ + -H "Content-Type: application/json" \ + -H "Authorization:Bearer {apiKey}" \ + -X POST \ + -d "{\"objectId\":\"0xA160cdAB225685dA1d56aa342Ad8841c3b53f291\",\"objectType\":\"address\",\"analysisType\":\"\",\"chainShortName\":\"eth\",\"depth\":\"\"}" +``` + +如上面示例代码,你可以通过访问 `https://api.zan.top/kyt/v1/score` 来获取某一个地址的相关数据。 + +## 总结 + +这一讲,我们介绍了如何使用 ZAN 提供的各种 Web3 技术服务。ZAN 提供了丰富的服务,有面向 DApp 开发者的,也有面向商业机构,安全团队,研究机构等的服务,你可以根据自己的需求选择合适的服务。 diff --git a/Topics/readme.md b/Topics/readme.md index 317a50c03..10de7cfee 100644 --- a/Topics/readme.md +++ b/Topics/readme.md @@ -14,6 +14,8 @@ **第7讲:Foundry,以Solidity为中心的开发工具包** 【[代码](./Tools/TOOL07_Foundry/readme.md)】 +**第8讲:ZAN,节点服务和合约审计等Web3技术服务** 【[文章](https://github.com/AmazingAng/WTFSolidity/blob/main/Topics/Tools/TOOL08_ZAN/readme.md)】 + ### `链上威胁分析` **第1讲:工具篇** 【[文章](https://github.com/AmazingAng/WTF-Solidity/tree/main/Topics/Onchain_debug//01_tools/)】 | 【[English](https://github.com/AmazingAng/WTF-Solidity/tree/main/Topics/Onchain_debug/01_tools/en/)】