├── README.md ├── courses ├── 1.md ├── 2.md └── 3.md └── img ├── 01.png ├── 02.png ├── 03.png ├── 04.png ├── 05.png ├── 06.png ├── 07.png ├── 08.png ├── 09.png ├── 10.png ├── 11.png ├── 12.png ├── 13.png └── 14.png /README.md: -------------------------------------------------------------------------------- 1 | # learn-solidity 2 | 3 | ``` 4 | /$$$$$$ /$$ /$$ /$$ /$$ /$$ 5 | /$$__ $$ | $$|__/ | $$|__/ | $$ 6 | | $$ \__/ /$$$$$$ | $$ /$$ /$$$$$$$ /$$ /$$$$$$ /$$ /$$ 7 | | $$$$$$ /$$__ $$| $$| $$ /$$__ $$| $$|_ $$_/ | $$ | $$ 8 | \____ $$| $$ \ $$| $$| $$| $$ | $$| $$ | $$ | $$ | $$ 9 | /$$ \ $$| $$ | $$| $$| $$| $$ | $$| $$ | $$ /$$| $$ | $$ 10 | | $$$$$$/| $$$$$$/| $$| $$| $$$$$$$| $$ | $$$$/| $$$$$$$ 11 | \______/ \______/ |__/|__/ \_______/|__/ \___/ \____ $$ 12 | /$$ | $$ 13 | | $$$$$$/ 14 | \______/ 15 | ``` 16 | 17 | 本课程致力于推广面对财富编程的以太坊智能合约语言 Solidity 18 | 19 | - 对于有代码基础的小伙伴, 可以通过此教程入门 20 | - 对于没有代码基础的小伙伴, 可以通过此教程了解部分以太坊的工作原理以及智能合约是什么, 甚至可以看懂比较简单的智能合约 21 | 22 | # 目录 23 | 24 | 1. [第一课](./courses/1.md#第一课) 25 | 26 | 1. [简介](./courses/1.md#简介) 27 | - [什么是以太坊智能合约?](./courses/1.md#什么是以太坊智能合约) 28 | - [什么是 Solidity?](./courses/1.md#什么是-solidity) 29 | - [以太坊账户模型](./courses/1.md#以太坊账户模型) 30 | - [以太坊交易](./courses/1.md#以太坊交易) 31 | 1. [准备工作](./courses/1.md#准备工作) 32 | 1. [Solidity](./courses/1.md#Solidity) 33 | - [文件结构](./courses/1.md#文件结构) 34 | - [常用类型](./courses/1.md#常用类型) 35 | - [全局变量和函数](./courses/1.md#全局变量和函数) 36 | - [使用例子](./courses/1.md#使用例子) 37 | 1. [Transaction 和 Call 的区别](./courses/1.md#transaction-和-call-的区别) 38 | 1. [第一个智能合约(ERC20)](./courses/1.md#第一个智能合约) 39 | 1. [课堂作业](./courses/1.md#课堂作业) 40 | 41 | 1. [第二课](./courses/2.md#第二课) 42 | 43 | 1. [Remix](./courses/2.md#Remix) 44 | - [编译智能合约](./courses/2.md#编译智能合约) 45 | - [部署、调试智能合约](./courses/2.md#部署、调试智能合约) 46 | - [手动认证智能合约](./courses/2.md#手动认证智能合约) 47 | 1. [Hardhat](./courses/2.md#Hardhat) 48 | - [Hardhat 项目构建](./courses/2.md#hardhat-项目构建) 49 | - [Hardhat 的使用](./courses/2.md#hardhat-的使用) 50 | - [自动认证智能合约](./courses/2.md#自动认证智能合约) 51 | 1. 示例代码讲解(OpenZepplin) 52 | - [SafeMath](https://github.com/OpenZeppelin/openzeppelin-contracts/blob/release-v3.2.0/contracts/math/SafeMath.sol) 53 | - [AccessControl](https://github.com/OpenZeppelin/openzeppelin-contracts/blob/release-v3.2.0/contracts/access/AccessControl.sol) 54 | - [ERC20](https://github.com/OpenZeppelin/openzeppelin-contracts/tree/release-v3.2.0/contracts/token/ERC20) 55 | - [ERC721](https://github.com/OpenZeppelin/openzeppelin-contracts/tree/release-v3.2.0/contracts/token/ERC721) 56 | 1. [课堂作业](./courses/2.md#课堂作业) 57 | 58 | 1. [第三课](./courses/3.md#第三课) 59 | 60 | 1. [UniswapV2 智能合约分析](./courses/3.md#uniswapv2-智能合约分析) 61 | - [什么是 Uniswap](./courses/3.md#什么是-uniswap) 62 | - [交易](./courses/3.md#交易) 63 | - [质押赎回流动性](./courses/3.md#质押赎回流动性) 64 | - [交易手续费](./courses/3.md#交易手续费) 65 | - [无偿损失](./courses/3.md#无偿损失) 66 | 1. SushiSwap 智能合约分析 67 | - [MasterChef](https://github.com/sushiswap/sushiswap/blob/master/contracts/MasterChef.sol) 68 | - [SushiBar](https://github.com/sushiswap/sushiswap/blob/master/contracts/SushiBar.sol) 69 | - [SushiMaker](https://github.com/sushiswap/sushiswap/blob/master/contracts/SushiMaker.sol) 70 | 1. MdexSwap 智能合约分析 71 | - [SwapMining](https://github.com/mdexSwap/contracts/blob/master/contracts/heco/SwapMining.sol) 72 | 1. [课堂作业](./courses/3.md#课堂作业) 73 | 74 | 1. 第四课 75 | 76 | 1. 智能合约常见漏洞 77 | - 重入攻击 78 | - 溢出攻击 79 | - 未初始化的存储指针 80 | - ... 81 | 1. 智能合约原理 82 | - merkle-patricia-trie 83 | - 智能合约运行原理 84 | - 一个简单的例子 85 | -------------------------------------------------------------------------------- /courses/1.md: -------------------------------------------------------------------------------- 1 | # 第一课 2 | 3 | ## 简介 4 | 5 | ### 什么是以太坊智能合约? 6 | 7 | 每个以太坊节点的内部都会运行一个 EVM(Ethereum Virtual Machine), 智能合约就是运行在 EVM 中的代码, 他通常具有一下一些特征 8 | 9 | - 分布式, 去中心化 10 | 11 | 智能合约一旦部署, 会在以太坊网络的所有节点中被复制和分发, 任何个人或组织无法把持 12 | 13 | - 具有一致性并且可验证 14 | 15 | 在同一个状态下(块高度, 哈希相同), 所有以太坊节点对于同一智能合约的相同输入参数的运行结果是一致的 16 | 17 | - 图灵完备 18 | 19 | - 不可篡改 20 | 21 | 运行结果会以 merkle-patricia-trie 的方式存储在区块中, 确保结果不会被篡改 22 | 23 | - 不可更新 24 | 25 | 合约代码一旦部署, 无法修改 26 | 27 | 但同时, 他也存在一些局限性 28 | 29 | - 无法进行网络请求 30 | 31 | 由于网络请求的结果是不确定的, 并且是可以被攻击的, 为了保证一致性, 智能合约无法进行网络请求 32 | 33 | - 只能持久化有限的数据, 并且费用高昂 34 | 35 | 由于智能合约被复制的特性, 数据在持久化时, 在所有节点都要复制一遍. 因此为了确保整个网络的稳定性, 只能使用一些简单的容器在以太坊状态树中对有限的数据进行持久化 36 | 37 | - 不可更新会导致漏洞难以被修复 38 | 39 | ### 什么是 Solidity? 40 | 41 | 理论上任何可以编译为 EVM 字节码的语言都可以用来编写智能合约, 而 Solidity 是目前主流的智能合约编写语言(其他的还有 Vyper), 其语法类似于 JavaScript 42 | 43 | ### 以太坊账户模型 44 | 45 | 在以太坊中, 一个账户包含四个字段, 分别是 46 | 47 | - balance 48 | 49 | 地址当前余额 50 | 51 | - nonce 52 | 53 | 地址的 nonce, 此地址每发起一笔交易, nonce 加 1. 若干笔 nonce 相同的交易中只有一笔会被打包. nonce 过大的交易会放入 queue 中, 等待打包. 可以简单的理解为, 是为了让用户控制交易顺序而设置的字段 54 | 55 | - codeHash 56 | 57 | 合约字节码的哈希, 根据此哈希可以在数据库中找到此地址对应的合约字节码, 合约账户才有此字段, 普通账户此字段用于为空 58 | 59 | - storageRoot(stateRoot) 60 | 61 | 账户状态树的根节点的哈希, 根据此哈希可以在数据库中找到一颗树的根节点(然后根据根节点可以找到这颗树中的任意一个节点), 这颗树里面记录了所有有关此地址的持久化数据 62 | 63 | 以太坊对普通账户和合约账户一视同仁. 一个地址可能是普通账户, 也可能是合约账户, 只取决于是否存在 codeHash 字段 64 | 65 | ### 以太坊交易 66 | 67 | - 交易 68 | 69 | 根据交易参数的不同, 可以分为: 普通转账交易、创建合约交易、合约调用交易. 在最新的 berlin 硬分叉之后, 前三种交易统称为 `LegacyTransaction`, 并且又增加了 `EIP2930AccessListTransaction` 交易 70 | 71 | - 交易收据 72 | 73 | 当一笔交易被以太坊节点打包时, 节点会为每一笔交易生成一个交易收据(receipt), 里面包含了这笔交易具体使用的 gas 数量以及输出的日志等信息 74 | 75 | 一笔普通的转账交易: 76 | 77 | ```json 78 | // transaction 79 | { 80 | "blockHash": "0x31b877d6a0b61e611bd8c52ddfdf90cbf095348ce534dabf84b257ae8a848bcb", 81 | "blockNumber": "0x98f", 82 | "from": "0xb4ec1f6419d66bfacebdd5b53fa895a636473c39", 83 | "gas": "0x5208", 84 | "gasPrice": "0x3b9aca00", 85 | "hash": "0xd6f56d259a4ab5963e133af8080955927182b3a12692cd8b4acbdca3a09192bb", 86 | "input": "0x", 87 | "nonce": "0x0", 88 | "to": "0x92b4df021a4621d07f2cbdce327bdcd437a69990", 89 | "transactionIndex": "0x0", 90 | "value": "0x56bc75e2d63100000", 91 | "v": "0x60b0", 92 | "r": "0xf79d64403c0e2f20f0bc609be32af46f554109e49d05763323b8688cd7da168f", 93 | "s": "0x7ef2b2142b9f8974d3de3db2216ed81f1c12827217e3d0bf67ce1490dba0f907" 94 | } 95 | ``` 96 | 97 | ```json 98 | // receipt 99 | { 100 | "blockHash": "0x31b877d6a0b61e611bd8c52ddfdf90cbf095348ce534dabf84b257ae8a848bcb", 101 | "blockNumber": "0x98f", 102 | "contractAddress": null, 103 | "cumulativeGasUsed": "0x5208", 104 | "from": "0xb4ec1f6419d66bfacebdd5b53fa895a636473c39", 105 | "gasUsed": "0x5208", 106 | "logs": [], 107 | "logsBloom": "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", 108 | "status": "0x1", 109 | "to": "0x92b4df021a4621d07f2cbdce327bdcd437a69990", 110 | "transactionHash": "0xd6f56d259a4ab5963e133af8080955927182b3a12692cd8b4acbdca3a09192bb", 111 | "transactionIndex": "0x0" 112 | } 113 | ``` 114 | 115 | 一笔创建合约的交易: 116 | 117 | ```json 118 | // transaction 119 | { 120 | "blockHash": "0x7678aad64c420eef77839cdd8a4cd562166cd520fc81e2d579b897dc1f60e35b", 121 | "blockNumber": "0xa09", 122 | "from": "0xb4ec1f6419d66bfacebdd5b53fa895a636473c39", 123 | "gas": "0x16387", 124 | "gasPrice": "0x1", 125 | "hash": "0xb539e05ec11e8b3cceeba78bbd98f95c8f47d48563b43320d46af229ea805622", 126 | "input": "0x6080604052348015600f57600080fd5b506040516100d93803806100d983398181016040526020811015603157600080fd5b50506098806100416000396000f3fe6080604052348015600f57600080fd5b506004361060285760003560e01c8063f42c13bf14602d575b600080fd5b60336035565b005b60405133907fb8a00d6d8ca1be30bfec34d8f97e55f0f0fd9eeb7fb46e030516363d4cfe1ad690600090a256fea2646970667358221220e8f1f8489264b1dfa4a65de6338e8d7952eff655284f07fefb9cd7d41a501ff164736f6c634300060200330000000000000000000000000000000000000000000000000000000000000001", 127 | "nonce": "0x3", 128 | "to": null, 129 | "transactionIndex": "0x0", 130 | "value": "0x0", 131 | "v": "0x60af", 132 | "r": "0x6db42ad11c6ef81771c480701b66551acbc31948028046ccbcc2815f364ccac1", 133 | "s": "0x7df760aedf93872323162babd8b2e2775bf2d51e4e84e46c5c2bb1da7dc1ee00" 134 | } 135 | ``` 136 | 137 | ```json 138 | // receipt 139 | { 140 | "blockHash": "0x7678aad64c420eef77839cdd8a4cd562166cd520fc81e2d579b897dc1f60e35b", 141 | "blockNumber": "0xa09", 142 | "contractAddress": "0xf4625b6c83201a695e37eb777d7954d7192fe1fa", 143 | "cumulativeGasUsed": "0x153d7", 144 | "from": "0xb4ec1f6419d66bfacebdd5b53fa895a636473c39", 145 | "gasUsed": "0x0153d7", 146 | "logs": [], 147 | "logsBloom": "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", 148 | "status": "0x1", 149 | "transactionHash": "0xb539e05ec11e8b3cceeba78bbd98f95c8f47d48563b43320d46af229ea805622", 150 | "transactionIndex": "0x0" 151 | } 152 | ``` 153 | 154 | 一笔调用合约的交易: 155 | 156 | ```json 157 | // transaction 158 | { 159 | "blockHash": "0xc4ca818e4b3bd10b179f92be311820fe2900db4843d2bec4269a6e70b1078323", 160 | "blockNumber": "0xa75", 161 | "from": "0xb4ec1f6419d66bfacebdd5b53fa895a636473c39", 162 | "gas": "0x5bd3", 163 | "gasPrice": "0x1", 164 | "hash": "0x235e1d1cc159a414dc20ee43ae5ae42de02ba19e135a91d16985db39a1ca21d8", 165 | "input": "0x34c9165b0000000000000000000000000000000000000000000000000000000000000001", 166 | "nonce": "0x5", 167 | "to": "0xf4625b6c83201a695e37eb777d7954d7192fe1fa", 168 | "transactionIndex": "0x0", 169 | "value": "0x0", 170 | "v": "0x60af", 171 | "r": "0x751ded72e7ae71c8909b61de26c74d0c4034387eabb2953db70d59d4bf979747", 172 | "s": "0xf31a04189b96fd67ba2c63149e6a1ef5a1ae5a59e34698a372d8154a4bb587d" 173 | } 174 | ``` 175 | 176 | ```json 177 | // receipt 178 | { 179 | "blockHash": "0xc4ca818e4b3bd10b179f92be311820fe2900db4843d2bec4269a6e70b1078323", 180 | "blockNumber": "0xa75", 181 | "contractAddress": null, 182 | "cumulativeGasUsed": "0x5973", 183 | "from": "0xb4ec1f6419d66bfacebdd5b53fa895a636473c39", 184 | "gasUsed": "0x5973", 185 | "logs": [ 186 | { 187 | "address": "0xf4625b6c83201a695e37eb777d7954d7192fe1fa", 188 | "blockHash": "0xc4ca818e4b3bd10b179f92be311820fe2900db4843d2bec4269a6e70b1078323", 189 | "blockNumber": "0xa75", 190 | "data": "0x", 191 | "logIndex": "0x0", 192 | "topics": [ 193 | "0xf950957d2407bed19dc99b718b46b4ce6090c05589006dfb86fd22c34865b23e", 194 | "0x000000000000000000000000b4ec1f6419d66bfacebdd5b53fa895a636473c39", 195 | "0x0000000000000000000000000000000000000000000000000000000000000001" 196 | ], 197 | "transactionHash": "0x235e1d1cc159a414dc20ee43ae5ae42de02ba19e135a91d16985db39a1ca21d8", 198 | "transactionIndex": "0x0" 199 | } 200 | ], 201 | "logsBloom": "0x00000000000000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000100000000040000000000080000000000000000000000000000000000040020000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000010000000000000000000040000008000000000000000000000000000000200000000000000000000000000000", 202 | "status": "0x1", 203 | "to": "0xf4625b6c83201a695e37eb777d7954d7192fe1fa", 204 | "transactionHash": "0x235e1d1cc159a414dc20ee43ae5ae42de02ba19e135a91d16985db39a1ca21d8", 205 | "transactionIndex": "0x0" 206 | } 207 | ``` 208 | 209 | 综上可得: 210 | 211 | - 普通转账时, `input` 为空 212 | - 创建合约时, `input` 为合约字节码及构造函数的参数,`to` 为 `null`, 并且在交易收据中会返回创建的合约的地址 213 | - 调用合约时, `input`为`function selector`加上调用参数, 并且如果合约打印日志的话, 可以在其收据中获取 214 | 215 | ## 准备工作 216 | 217 | 推荐使用 Remix 作为 IDE, 因为他便捷简单, 适合初学者 218 | 219 | 1. 打开[Remix IDE](http://remix.ethereum.org/) 220 | 221 | ![](../img/01.png) 222 | 223 | 2. 切换到文件浏览器页面, 新建文件并编写合约 224 | 225 | ```solidity 226 | // SPDX-License-Identifier: MIT 227 | 228 | pragma solidity 0.6.2; 229 | 230 | contract MyContract { 231 | function sum(uint256 a, uint256 b) external pure returns(uint256 c) { 232 | c = a + b; 233 | } 234 | } 235 | ``` 236 | 237 | ![](../img/02.png) 238 | 239 | 3. 切换到编译器选项页面, 编译合约 240 | 241 | ![](../img/03.png) 242 | 243 | 4. 切换到部署/调用合约页面, 部署合约 244 | 245 | ![](../img/04.png) 246 | 247 | 5. 切换到部署/调用合约页面, 与合约交互 248 | 249 | ![](../img/05.png) 250 | 251 | ## Solidity 252 | 253 | [官方文档](https://docs.soliditylang.org/en/latest/) 254 | 255 | ### 文件结构 256 | 257 | #### 协议声明 258 | 259 | 协议声明需要写在每个文件最开头的地方 260 | 261 | ```solidity 262 | // SPDX-License-Identifier: MIT 263 | ``` 264 | 265 | ```solidity 266 | // SPDX-License-Identifier: GPL-3.0 267 | ``` 268 | 269 | #### 编译器版本声明 270 | 271 | ```solidity 272 | pragma solidity ^0.4.0; 273 | ``` 274 | 275 | `0.4.0` 以上但是 `0.5.0` 以下 276 | 277 | ```solidity 278 | pragma solidity 0.6.2; 279 | ``` 280 | 281 | ```solidity 282 | pragma solidity =0.6.2; 283 | ``` 284 | 285 | 必须是`0.6.2` 286 | 287 | ```solidity 288 | pragma solidity >=0.6.2 <0.8.5; 289 | ``` 290 | 291 | `0.6.2` 以上但是 `0.8.5` 以下 292 | 293 | 编译器版本声明的行为基本与 npm 的行为一致 294 | 295 | #### 导入其他的源文件 296 | 297 | ```solidity 298 | import "filename"; 299 | import * as symbolName from "filename"; 300 | import { symbol1 as alias, symbol2 } from "filename"; 301 | import "filename" as symbolName; 302 | ``` 303 | 304 | 导入声明的行为基本与 typescript 一致, 一般使用第一种即可 305 | 306 | ```solidity 307 | import "https://github.com/.../filename.sol"; 308 | ``` 309 | 310 | Remix 提供了一种特殊的导入方式, 可以直接通过 url 导入文件 311 | 312 | #### 注释 313 | 314 | ```solidity 315 | // This is a single-line comment. 316 | 317 | /* 318 | This is a 319 | multi-line comment. 320 | */ 321 | ``` 322 | 323 | ### 常用类型 324 | 325 | #### `bool`布尔类型 326 | 327 | 布尔类型只能是`true`或者`false` 328 | 329 | #### 整数类型 330 | 331 | 无符号整数范围为`uint8`,`uint16`,`uint32`, ..., `uint256`, 有符号整数范围为`int8`,`int16`,`int32`, ...,`int256` 332 | 其中`uint`/`int`是`uint256`/`int256`的别名 333 | 334 | 整数之间除法的结果也是整数(`5`/`2`=`2`) 335 | 336 | #### `Address`地址类型 337 | 338 | 地址类型持有一个 20 字节的数组(和以太坊地址长度一样) 339 | 340 | 成员及方法: 341 | 342 | - `balance` 地址当前的余额 343 | - `transfer(uint256 amount)` 向该地址转账(接受转账的地址必须被`payable`修饰) 344 | 345 | ```solidity 346 | address addr = 0xb4eC1F6419d66bfaCEBdd5b53FA895a636473C39; 347 | // 无法为不是payable的地址转账 348 | // addr.transfer(1 ether); 349 | // 转换后可以转账 350 | payable(addr).transfer(1 ether); 351 | 352 | address payable addr2 = 0xb4eC1F6419d66bfaCEBdd5b53FA895a636473C39; 353 | // 可以转账 354 | addr2.transfer(1 ether); 355 | ``` 356 | 357 | - `send`,`call`,`callcode`,`delegatecall` 底层函数调用, 不建议直接使用, 具体使用参考文档 358 | 359 | #### 固定长度的字节数组 360 | 361 | 从`bytes1`,`bytes2`,`bytes3`, ..., 到`bytes32` 362 | 其中`byte`是`bytes1`的别名 363 | 364 | 成员及方法: 365 | 366 | - `length` 数组长度 367 | 368 | #### 变长数组 369 | 370 | `bytes`是变长字节数组的别名(不是单独的类型) 371 | 372 | `string`是变长 UTF-8 编码字符数组的别名(不是单独的类型) 373 | 374 | 成员及方法: 375 | 376 | - `length` 数组长度 377 | - `push(x)` 增加一个元素到数组末尾 378 | - `pop()` 移除一个数组末尾的元素 379 | 380 | 在内存中创建变长数组: 381 | 382 | ```solidity 383 | contract C { 384 | function f(uint len) public pure { 385 | uint[] memory a = new uint[](7); 386 | bytes memory b = new bytes(len); 387 | // Here we have a.length == 7 and b.length == len 388 | a[6] = 8; 389 | } 390 | } 391 | ``` 392 | 393 | 变长数组作为成员变量: 394 | 395 | ```solidity 396 | contract C { 397 | uint256[] public list; 398 | // 一般需要提供一个获取数组长度的公开方法, 这样外部才能知道数组的长度 399 | function len() external view returns (uint256) { 400 | return list.length; 401 | } 402 | function f(uint256 ele) external { 403 | list.push(ele); 404 | } 405 | } 406 | ``` 407 | 408 | 删除没有用的数组元素(删除元素可以为整个网络节省出更多的状态树存储空间, 并且还有 gas 费返还, 是一个良好的习惯): 409 | 410 | ```solidity 411 | contract C { 412 | uint256[] public array = [1,2,3]; 413 | // 一般需要提供一个获取数组长度的公开方法, 这样外部才能知道数组的长度 414 | function len() external view returns (uint256) { 415 | return array.length; 416 | } 417 | // 被删除的元素会变成下一个元素, 数组长度减少1 418 | function removeAtIndexAndShift(uint index) external { 419 | if (index >= array.length) return; 420 | for (uint i = index; i < array.length-1; i++) { 421 | array[i] = array[i+1]; 422 | } 423 | array.pop(); 424 | } 425 | // 被删除的元素会变成0 426 | function removeAtIndex(uint index) external { 427 | if (index >= array.length) return; 428 | delete array[index]; 429 | } 430 | } 431 | ``` 432 | 433 | #### `mapping`哈希表 434 | 435 | `mapping(_KeyType => _ValueType)` 声明一个从`_KeyType`到`_ValueType`的哈希表, `_KeyType`只能是内置类型 436 | 437 | 由于 EVM 特殊的存储结构 438 | 439 | - `mapping` 无法作为局部变量, 只能作为合约或结构体的成员变量 440 | - `mapping` 无法遍历(但有些第三方库实现了这个功能, 本质是一个 mapping 加上一个 array) 441 | 442 | 可以使用`[]`操作符与哈希表中的值交互: 443 | 444 | ```solidity 445 | contract C { 446 | mapping(address => uint256) public balanceOf; 447 | function f() public { 448 | address addr = 0xb4eC1F6419d66bfaCEBdd5b53FA895a636473C39; 449 | balanceOf[addr] = 100; 450 | } 451 | } 452 | ``` 453 | 454 | 判断键是否存在: 455 | 456 | ```solidity 457 | contract C { 458 | struct S { 459 | uint256 amount; 460 | address sender; 461 | } 462 | mapping(address => S) public userInfo; 463 | mapping(address => uint256) public balanceOf; 464 | 465 | function f1() public view returns(bool) { 466 | return balanceOf[msg.sender] == 0; 467 | } 468 | 469 | function f2() public view returns(bool) { 470 | S storage s = userInfo[msg.sender]; 471 | return s.sender == address(0); 472 | } 473 | } 474 | ``` 475 | 476 | 删除没有用的键(删除键可以为整个网络节省出更多的状态树存储空间, 并且还有 gas 费返还, 是一个良好的习惯): 477 | 478 | ```solidity 479 | contract C { 480 | mapping(address => uint256) public balanceOf; 481 | 482 | function f() public { 483 | delete balanceOf[msg.sender]; 484 | } 485 | } 486 | ``` 487 | 488 | ### 全局变量和函数 489 | 490 | #### 单位 491 | 492 | ```solidity 493 | assert(1 wei == 1); 494 | assert(1 gwei == 1e9); 495 | assert(1 ether == 1e18); 496 | ``` 497 | 498 | ```solidity 499 | assert(1 == 1 seconds); 500 | assert(1 minutes == 60 seconds); 501 | assert(1 hours == 60 minutes); 502 | assert(1 days == 24 hours); 503 | assert(1 weeks == 7 days); 504 | ``` 505 | 506 | #### 全局变量 507 | 508 | `block.chainid`(`uint`) chainID 509 | 510 | `block.coinbase`(`address payable`) 当前区块矿工地址 511 | 512 | `block.difficulty`(`uint`) 当前区块难度 513 | 514 | `block.gaslimit`(`uint`) 当前区块 gasLimit 515 | 516 | `block.number`(`uint`) 当前区块高度 517 | 518 | `block.timestamp`(`uint`) 当前区块时间戳 519 | 520 | `msg.data`(`bytes calldata`) 交易的 input 521 | 522 | `msg.sender`(`address`) 合约的直接调用者 523 | 524 | `msg.sig`(`bytes4`) 交易 input 的前四个字节(`function selector`) 525 | 526 | `msg.value`(`uint`) 交易转账的金额 527 | 528 | `tx.gasprice`(`uint`) 交易设置的 gas 价格 529 | 530 | `tx.origin`(`address`) 发起交易的地址 531 | 532 | `this`(`address`) 合约自己的地址 533 | 534 | #### 全局函数 535 | 536 | `gasleft() returns (uint256)` 返回剩余的 gas 数量 537 | 538 | `blockhash(uint blockNumber) returns (bytes32)` 返回指定高度的哈希 539 | 540 | `abi.encodePacked(...) returns (bytes memory)` abi 非标准模式编码, 这样编码出来更节省空间, 一般用于计算多个字段的哈希. 由于是非标准模式编码, 其结果是模糊的, 因此无法解码 541 | 542 | `assert(bool condition)` 如果 `condition` 为 `false` 则触发回滚 543 | 544 | `require(bool condition)` 如果 `condition` 为 `false` 则触发回滚 545 | 546 | `require(bool condition, string memory message)`如果 `condition` 为 `false` 则触发回滚, 并展示错误信息 547 | 548 | `keccak256(bytes memory) returns (bytes32)` 计算 keccak256 哈希 549 | 550 | `sha256(bytes memory) returns (bytes32)` 计算 sha256 哈希 551 | 552 | `ripemd160(bytes memory) returns (bytes20)` 计算 ripemd160 哈希 553 | 554 | `ecrecover(bytes32 hash, uint8 v, bytes32 r, bytes32 s) returns (address)` 恢复签名的地址 555 | 556 | `selfdestruct(address payable recipient)` 向 `recipient` 发送合约中所有的 ETH, 并自毁(自毁后合约中的数据都将从状态树中删除, 并且无法再调用) 557 | 558 | ### 使用例子 559 | 560 | #### 声明合约及其成员变量 561 | 562 | ```solidity 563 | // 声明合约使用contract关键字, Solidity 中的合约就是其他语言中的类 564 | contract SimpleStorage { 565 | // 声明枚举使用enum关键字 566 | enum State { Created, Locked, Inactive } 567 | // 声明结构体使用struct关键字 568 | struct Voter { 569 | uint weight; 570 | bool voted; 571 | address delegate; 572 | uint vote; 573 | // 该结构体持有一个枚举变量 574 | State state; 575 | } 576 | 577 | // 声明一个uint256类型的成员变量, 并且他可以直接被外部或内部访问 578 | uint public storedData; 579 | // 声明一个从address到Voter的哈希表, 并且他只能从内部访问 580 | mapping(address => Voter) private voters; 581 | 582 | // 声明合约的构造函数, 构造函数必须是public的, 构造函数在部署合约的时候会被调用 583 | constructor(uint256 index) public { 584 | // ... 585 | } 586 | } 587 | ``` 588 | 589 | #### 声明函数 590 | 591 | ```solidity 592 | contract SimpleStorage { 593 | function func1() external { 594 | // ... 595 | } 596 | 597 | function func2(uint256 num1, uint256 num2) external payable returns(uint256 sum1, uint256 sum2) { 598 | // ... 599 | } 600 | } 601 | ``` 602 | 603 | 函数使用`function`关键字进行声明, 依次是 604 | 605 | ```solidity 606 | function 函数名(类型1 形参1, 类型2 形参2) external/public/internal/private pure/view/payable 607 | ``` 608 | 609 | 如果有返回值的话最后需要加上`returns (类型1, 类型2)`来声明返回类型 610 | 611 | 其中: 612 | 613 | - `external`关键字代表此函数只能被外部访问 614 | - `public`关键字代表此函数可以同时被外部和内部访问 615 | - `internal`关键字代表此函数可以被内部和子类访问 616 | - `private`关键字代表此函数只能被内部访问 617 | 618 | 其中: 619 | 620 | - `pure`关键字代表此函数无法访问状态树 621 | - `view`关键字代表此函数可以访问状态树, 但无法修改 622 | - `payable`关键字代表此函数可以接受转账(调用时, 交易中的`value`可以不为 0) 623 | - 如果需要访问并修改状态树, 但不接受转账, 则在此处无需加关键字 624 | 625 | #### 声明并使用修饰符 626 | 627 | ```solidity 628 | contract SimpleStorage { 629 | address public seller1; 630 | address public seller2; 631 | 632 | modifier onlySeller() { // Modifier 633 | require(msg.sender == seller1); 634 | _; 635 | require(msg.sender == seller2); 636 | } 637 | 638 | function func() external onlySeller { 639 | // ... 640 | } 641 | } 642 | ``` 643 | 644 | 修饰符使用`modifier`关键字进行声明, 一般用于对某些字段进行统一的检查, 用`_;`表示继续运行原函数的代码 645 | 646 | #### 声明并触发事件 647 | 648 | ```solidity 649 | contract SimpleAuction { 650 | event HighestBidIncreased(address indexed bidder, uint amount); // Event 651 | 652 | function bid() public payable { 653 | // ... 654 | emit HighestBidIncreased(msg.sender, msg.value); // Triggering event 655 | } 656 | } 657 | ``` 658 | 659 | 触发事件后, 可以在 receipt 中找到相关的信息, 如果订阅了这种事件, 也可以收到节点的通知 660 | 661 | - `indexed`关键字代表以太坊会为事件中的这个字段创建索引, 一个事件最多只能为三个参数创建索引, 建立索引可以提高查询日志的效率 662 | 663 | #### 声明并使用接口 664 | 665 | 接口类似合约, 但不包含任何具体的实现, 一般用于跨合约调用以及解耦合, e.g. 666 | 667 | ```solidity 668 | // MyInterface.sol 669 | // SPDX-License-Identifier: MIT 670 | 671 | pragma solidity 0.6.2; 672 | 673 | interface MyInterface { 674 | function sum(uint256, uint256) external view returns(uint256); 675 | } 676 | ``` 677 | 678 | ```solidity 679 | // MyContract.sol 680 | // SPDX-License-Identifier: MIT 681 | 682 | pragma solidity 0.6.2; 683 | 684 | import "./MyInterface.sol"; 685 | 686 | contract MyContract is MyInterface { 687 | function sum(uint256 a, uint256 b) external view override returns(uint256 c) { 688 | c = a + b; 689 | } 690 | } 691 | ``` 692 | 693 | ```solidity 694 | // YourContract.sol 695 | // SPDX-License-Identifier: MIT 696 | 697 | pragma solidity 0.6.2; 698 | 699 | import "./MyInterface.sol"; 700 | 701 | contract YourContract { 702 | MyInterface public myInterface; 703 | 704 | constructor(address addr) public { 705 | myInterface = MyInterface(addr); 706 | } 707 | 708 | function calc(uint256 a, uint256 b) external view returns (uint256 c) { 709 | c = myInterface.sum(a, b); 710 | } 711 | } 712 | ``` 713 | 714 | #### 声明并使用库 715 | 716 | ```solidity 717 | // SPDX-License-Identifier: MIT 718 | 719 | pragma solidity 0.6.2; 720 | 721 | library MyLibrary { 722 | function add(uint256 self, uint256 a) public pure returns(uint256 b) { 723 | b = self + a; 724 | require(b >= self, "addition overflow"); 725 | } 726 | function sub(uint256 self, uint256 a) public pure returns(uint256 b) { 727 | require(self >= a, "subtraction overflow"); 728 | b = self - a; 729 | } 730 | } 731 | 732 | contract MyContract { 733 | using MyLibrary for uint256; 734 | 735 | // 使用using for 736 | function calc1(uint256 a) public pure returns(uint256 b) { 737 | b = a.add(1).sub(2); 738 | } 739 | 740 | // 不使用using for 741 | function calc2(uint256 a) public pure returns(uint256 b) { 742 | b = MyLibrary.sub(MyLibrary.add(a, 1), 2); 743 | } 744 | } 745 | ``` 746 | 747 | 库类似合约, 但库不能被实例化. 使用`using lib for xxx`后, 库中所有的函数都会变成`xxx`类的成员函数, 并且函数的第一个参数都会被自动换成`xxx`类的实例 748 | 749 | ## Transaction 和 Call 的区别 750 | 751 | ```solidity 752 | // SPDX-License-Identifier: MIT 753 | 754 | pragma solidity 0.6.2; 755 | 756 | contract MyContract { 757 | uint256 public storageData; 758 | uint256[] public storageList; 759 | mapping(address => uint256) public storageMap; 760 | 761 | // 一般需要提供一个获取数组长度的公开方法, 这样外部才能知道数组的长度 762 | function len() external view returns (uint256) { 763 | return storageList.length; 764 | } 765 | 766 | function sum(uint256 a, uint256 b) external pure returns(uint256 c) { 767 | c = a + b; 768 | } 769 | 770 | function set(uint256 num) external { 771 | storageData = num; 772 | } 773 | 774 | function get() external view returns(uint256) { 775 | return storageData + 1; 776 | } 777 | } 778 | ``` 779 | 780 | - 外部可以直接查询设置为 `public` 的成员变量 781 | - “黄色的方法”代表`payable`或任何需要改变以太坊状态树的方法, 因此需要发起一笔交易, 等待交易被打包后, 才能拿到结果 782 | - “蓝色的方法”代表`pure`或`view`, 不会改变以太坊状态树, 因只需要进行几次只读的查询操作就能拿到结果 783 | 784 | ## 第一个智能合约 785 | 786 | ```solidity 787 | // SPDX-License-Identifier: MIT 788 | 789 | pragma solidity 0.6.2; 790 | 791 | contract MyERC20 { 792 | // 用户余额 793 | mapping(address => uint256) public balanceOf; 794 | // 总发行量 795 | uint256 public totalSupply = 0; 796 | // 拥有者 797 | address public owner; 798 | 799 | // 转账事件 800 | event Transfer(address indexed from, address indexed to, uint256 indexed amount); 801 | 802 | modifier onlyOwner() { 803 | require(msg.sender == owner); 804 | _; 805 | } 806 | 807 | constructor() public { 808 | owner = msg.sender; 809 | } 810 | 811 | // 增发 812 | function mint(address to, uint256 amount) external onlyOwner { 813 | require(amount > 0, "amount must be greater than 0"); 814 | balanceOf[to] += amount; 815 | totalSupply += amount; 816 | emit Transfer(address(0), to, amount); 817 | } 818 | 819 | // 销毁 820 | function burn(uint256 amount) external { 821 | require(amount > 0, "amount must be greater than 0"); 822 | require(balanceOf[msg.sender] >= amount, "burn amount exceeds balance"); 823 | balanceOf[msg.sender] -= amount; 824 | totalSupply -= amount; 825 | emit Transfer(msg.sender, address(0), amount); 826 | } 827 | 828 | // 转账 829 | function transfer(address to, uint256 amount) external { 830 | require(amount > 0, "amount must be greater than 0"); 831 | require(balanceOf[msg.sender] >= amount, "transfer amount exceeds balance"); 832 | require(msg.sender != to, "invalid transfer"); 833 | balanceOf[msg.sender] -= amount; 834 | balanceOf[to] += amount; 835 | emit Transfer(msg.sender, to, amount); 836 | } 837 | } 838 | ``` 839 | 840 | ## 课堂作业 841 | 842 | 实现一个银行合约存储 ETH, 实现以下功能 843 | 844 | - 任何人可以充值任意数量的 ETH 到自己的账户(提示: 充值的函数需要加上`payable`以接受转账, 可以通过`msg.value`获取转账金额) 845 | - 任何人可以转账自己的任意数量的 ETH 到别人的账户 846 | - 任何人可以随时提取自己的任意数量的 ETH 847 | - 需要提供查询余额的方法 848 | - 充值、提现、转账都需要发出对应的事件 849 | 850 | 请将结果发到, 同时附上名字 851 | 852 |
853 | 参考答案 854 | 855 | ```solidity 856 | // SPDX-License-Identifier: MIT 857 | 858 | pragma solidity 0.6.2; 859 | 860 | contract MyBank { 861 | // 用户余额 862 | mapping(address => uint256) public balanceOf; 863 | 864 | // 充值事件 865 | event Deposit(address indexed from, uint256 indexed amount); 866 | // 转账事件 867 | event Transfer(address indexed from, address indexed to, uint256 indexed amount); 868 | // 提现事件 869 | event Withdraw(address indexed from, uint256 indexed amount); 870 | 871 | // 充值 872 | function deposit() external payable { 873 | require(msg.value > 0, "value must be greater than 0"); 874 | balanceOf[msg.sender] += msg.value; 875 | emit Deposit(msg.sender, msg.value); 876 | } 877 | 878 | // 提现 879 | function withdraw(uint256 amount) external { 880 | require(amount > 0, "amount must be greater than 0"); 881 | require(balanceOf[msg.sender] >= amount, "withdraw amount exceeds balance"); 882 | balanceOf[msg.sender] -= amount; 883 | msg.sender.transfer(amount); 884 | emit Withdraw(msg.sender, amount); 885 | } 886 | 887 | // 转账 888 | function transfer(address to, uint256 amount) external { 889 | require(amount > 0, "amount must be greater than 0"); 890 | require(balanceOf[msg.sender] >= amount, "transfer amount exceeds balance"); 891 | require(msg.sender != to, "invalid transfer"); 892 | balanceOf[msg.sender] -= amount; 893 | balanceOf[to] += amount; 894 | emit Transfer(msg.sender, to, amount); 895 | } 896 | } 897 | ``` 898 | 899 |
900 | -------------------------------------------------------------------------------- /courses/2.md: -------------------------------------------------------------------------------- 1 | # 第二课 2 | 3 | ## Remix 4 | 5 | [Remix IDE](http://remix.ethereum.org/) 6 | 7 | ### 编译智能合约 8 | 9 | #### 编译面版介绍 10 | 11 | ![](../img/06.png) 12 | 13 | 1. 选择编译器版本, 大多是时候 IDE 会根据文件头部的声明自动选择 14 | 15 | 2. 选择要编译的语言, 默认 Solidity 即可 16 | 17 | 3. 根据以太坊硬分叉选择 EVM 的版本, 每个硬分叉都为 EVM 增加了新的特性, 如果不是想测试旧版本的 EVM 的话, 默认 default 即可 18 | 19 | 4. 是否自动编译, 勾选的话, 每次修改完都会自动编译一次 20 | 21 | 5. 是否启用编译优化以及编译优化等级, 一般选用 200 即可, 优化后的合约部署成本更低 22 | 23 | 6. 是否忽略警告 24 | 25 | 7. 编译合约 26 | 27 | 8. 选择要编译的合约`合约名(文件名.sol)` 28 | 29 | 9. 查看编译细节, 见[编译细节面板介绍](#编译细节面板介绍) 30 | 31 | 10. 复制合约 ABI(Application Binary Interface), ABI 是一个很大的 json, 一般用于外部和合约交互 32 | 33 | 11. 复制合约字节码, 字节码就是合约编译的结果 34 | 35 | #### 编译细节面板介绍 36 | 37 | ![](../img/07.png) 38 | 39 | 编译细节面板包含诸多细节, 一般只需要关心`FUNCTIONHASHES`, 这就是上一课所说的`function selector`.如果手动与合约交互, 可能会用到这个 40 | 41 | ### 部署、调试智能合约 42 | 43 | #### 部署、调试面版介绍 44 | 45 | ![](../img/08.png) 46 | 47 | 1. 选择运行的网络环境 48 | 49 | - JavaScript VM 50 | 51 | 使用在浏览器内存中运行的一个虚拟的区块链(每发起一笔交易, 都会为其生成一个新的区块, 但智能合约方面的行为与 EVM 完全一致)作为运行环境. 如果选择此选项, `ACCOUNT`中会自动生成若干个余额为 100 ETH 的账户用于测试 52 | 53 | - Injected Web3 54 | 55 | 使用浏览器中的 Web3 插件(一般是 MetaMask)当前的环境作为运行环境. 比如, 如果要链接到 GXChain2.0 的 Testnet, 需要先在 MetaMask 中添加并切换到自定义网络地址, 然后在 Remix 中选择 `Injected Web3` 56 | 57 | - Web3 Provider 58 | 59 | 使用手动输入的以太坊节点 RPC 地址作为运行环境, 如 `http://127.0.0.1:8545` 60 | 61 | 2. 选择调用合约的账户, 点击加号可以增加一个账户(只在`JavaScript VM`时可用) 62 | 63 | 3. 设置交易的 `GasLimit`, 有时候合约部署失败, 可能是因为这个数值低了, 需要注意 64 | 65 | 4. 设置交易附带的转账金额及单位, 只有 `payable` 的方法可以接收转账, 如果不是 `payable`, value 必须为 0 66 | 67 | 5. 选择需要部署的合约`合约名 - 文件名.sol`, 当文件中有多个合约的时候, 请确认合约名字是否是要部署的合约 68 | 69 | 6. 部署合约到当前的运行环境 70 | 71 | 7. 如果合约已经部署, 可以填入合约的地址, 并点击`At Address`即可 72 | 73 | 8. 已部署的合约列表, 可以在此处与合约交互, 见[合约交互面板介绍](#合约交互面板介绍) 74 | 75 | - `🗑️`可以立即清除所有合约 76 | 77 | - `x`清除指定的合约 78 | 79 | - `📋`复制合约地址 80 | 81 | 9. 当一笔交易发出后, 可以在命令行查看交易的具体参数 82 | 83 | #### 合约交互面板介绍 84 | 85 | ![](../img/09.png) 86 | 87 | 1. 黄色按钮代表此方法会修改以太坊状态树, 需要发起一笔交易才能调用 88 | 89 | 2. 蓝色按钮代表此方法不会修改以太坊状态树, 只需要进行一次`CALL`就可以调用 90 | 91 | - `📋`复制底层调用参数(`function selector` + 调用参数) 92 | 93 | 3. `CALL`的返回结果`返回参数下标: 类型: 值`, 只有`CALL`并且方法有返回参数时, 才可以在这里获取返回参数. 发起交易不能获得合约的返回值. 比如有一个函数`function func() external pure returns(uint256, string memory);`, 调用结果可以是 94 | 95 | ``` 96 | 0: uint256: 1 97 | 1: string: xxx 98 | ``` 99 | 100 | ### 手动认证智能合约 101 | 102 | [Etherscan](https://etherscan.io/) 103 | 104 | ![](../img/10.png) 105 | ![](../img/11.png) 106 | ![](../img/12.png) 107 | 108 | ## Hardhat 109 | 110 | [Hardhat Document](https://hardhat.org/getting-started/) 111 | 112 | 示例项目: [hardhat-erc20-example](https://github.com/samlior/hardhat-erc20-example) 113 | 114 | ### Hardhat 项目构建 115 | 116 | 1. 初始化(可通过 Fork 示例项目跳过以下步骤) 117 | 118 | 1. `npm i -g hardhat`安装 hardhat 119 | 120 | 2. 使用创建目录, 并在新目录下运行`npx hardhat`, 初始化项目 121 | 122 | 3. `npm i prettier -D`安装 prettier, 并通过`.prettierrc.js`配置 123 | 124 | 4. 通过`hardhat.config.js`配置 hardhat 125 | 126 | 2. 编写合约(在 contracts 目录下) 127 | 128 | 3. 编写部署脚本(在 deploy 目录下) 129 | 130 | [示例脚本](https://github.com/samlior/hardhat-erc20-example/blob/master/deploy/MyERC20.js) 131 | 132 | 4. 编写需要的任务脚本(在 tasks 目录下) 133 | 134 | [示例脚本](https://github.com/samlior/hardhat-erc20-example/blob/master/tasks/index.js) 135 | 136 | 5. 编写单元测试脚本(在 test 目录下) 137 | 138 | [示例脚本](https://github.com/samlior/hardhat-erc20-example/blob/master/test/MyERC20.js) 139 | 140 | ### Hardhat 的使用 141 | 142 | #### 设置环境变量 143 | 144 | ```sh 145 | # infura 的 api key 146 | export INFURA_API_KEY=xxx 147 | # 部署合约的账户的助记词 148 | export MNEMONIC="test test test test test test test test test test test junk" 149 | # 部署合约的账户的地址, 需要和助记词匹配 150 | export DEV_ADDR=0x...abc 151 | # etherscan 的 api key, 用于验证合约 152 | export ETHERSCAN_KEY=xxx 153 | ``` 154 | 155 | #### 安装依赖 156 | 157 | ```sh 158 | npm i 159 | ``` 160 | 161 | #### 编译 162 | 163 | ```sh 164 | npm run build 165 | ``` 166 | 167 | 以上命令实际上运行的是`npx hardhat compile`. 运行后, hardhat 会自动加载`hardhat.config.js`中的配置, 对`contracts`目录下的所有文件进行编译, 编译后的结果(包括 ABI, 字节码等)都将存储在`artifacts`文件夹下. hardhat 提供了各种方法加载合约 ABI 或字节码, 因此一般不需要关心`artifacts`文件夹下的具体内容 168 | 169 | #### 部署 170 | 171 | ```sh 172 | # 部署到goerli测试网络 173 | npm run deploy:goerli 174 | # 部署到bsc 175 | npm run deploy:bsc 176 | # 部署到本地测试节点 177 | npm run node 178 | ``` 179 | 180 | `npm run deploy:goerli`实际上运行的是`npx hardhat --network goerli deploy`, 其中 181 | 182 | - `--network` 声明了网络的名称(网络信息配置在`hardhat.config.js`中) 183 | - `deploy` 是 hardhat 内置的一个任务, 调用以后会运行 deploy 目录下的所有脚本 184 | 185 | `npm run node`实际上运行的是`npx hardhat node`, 这个命令会让 hardhat 在本地运行一个虚拟的区块链, 默认端口是 8545, 并且在区块链开始运行的同时, 会运行一次 deploy 任务(也就是说会把所有合约部署好) 186 | 187 | 合约部署完成后, 会将合约的名字、部署后的地址等信息放在`deployments`目录下, 只要不删除此目录下的内容, 之后就可以直接通过合约的名字获取合约地址, ABI 等信息, 比如 188 | 189 | ```js 190 | task("balance:erc20", "Prints an account's ERC20 balance") 191 | .addParam("account", "User account") 192 | .setAction(async (taskArgs, { deployments, web3 }) => { 193 | // ... 194 | const { getArtifact, get } = deployments; 195 | // 通过合约名字获取合约地址 196 | const addr = await get("MyERC20"); 197 | const contract = new web3.eth.Contract( 198 | // 通过合约名字获取合约 ABI, 用于初始化 web3 contract 对象 199 | (await getArtifact("MyERC20")).abi, 200 | addr, 201 | from 202 | ); 203 | // ... 204 | }); 205 | ``` 206 | 207 | #### 运行任务脚本 208 | 209 | 任务脚本可以完成一系列预设的逻辑, 通过以下方式运行 210 | 211 | ```sh 212 | npx hardhat --network networkName taskName --taskOption xxx 213 | ``` 214 | 215 | #### 运行单元测试脚本 216 | 217 | ```sh 218 | npm run test 219 | ``` 220 | 221 | ### 自动认证智能合约 222 | 223 | **认证合约时, 请先确保可以正常访问[api.etherscan.io](https://api.etherscan.io)** 224 | 225 | ```sh 226 | npx hardhat verify --network mainnet 0x...abc "LV Coin" "LV" "18" 227 | ``` 228 | 229 | 参数依次是`合约地址` `构造参数1` `构造参数2`, 不用关心构造参数的类型, hardhat 会自动处理 230 | 231 | ## 课堂作业 232 | 233 | ```solidity 234 | // SPDX-License-Identifier: MIT 235 | 236 | pragma solidity 0.6.2; 237 | 238 | // Remix 239 | import "https://github.com/OpenZeppelin/openzeppelin-contracts/blob/release-v3.2.0/contracts/math/SafeMath.sol"; 240 | import "https://github.com/OpenZeppelin/openzeppelin-contracts/blob/release-v3.2.0/contracts/token/ERC20/SafeERC20.sol"; 241 | import "https://github.com/OpenZeppelin/openzeppelin-contracts/blob/release-v3.2.0/contracts/utils/ReentrancyGuard.sol"; 242 | import "https://github.com/OpenZeppelin/openzeppelin-contracts/blob/release-v3.2.0/contracts/access/Ownable.sol"; 243 | 244 | // Hardhat 245 | // import "@openzeppelin/contracts/math/SafeMath.sol"; 246 | // import "@openzeppelin/contracts/token/ERC20/SafeERC20.sol"; 247 | // import "@openzeppelin/contracts/utils/ReentrancyGuard.sol"; 248 | // import "@openzeppelin/contracts/access/Ownable.sol"; 249 | 250 | contract MyUniswap is Ownable, ReentrancyGuard { 251 | using SafeMath for uint256; 252 | using SafeERC20 for IERC20; 253 | 254 | // tokenA的地址 255 | IERC20 public tokenA; 256 | // tokenB的地址 257 | IERC20 public tokenB; 258 | 259 | // x个A可以换 x * priceAToB 个B 260 | // x个B可以换 x / priceAToB 个A 261 | uint256 public priceAToB; 262 | 263 | // 记录是否以及初始化 264 | bool public initialized = false; 265 | 266 | // 初始化函数, 只有owner可以调用. 设置两种代币的地址, 初始化金额, 价格等参数 267 | function initialize(address _tokenA, uint256 tokenAInitAmount, address _tokenB, uint256 tokenBInitAmount, uint256 _priceAToB) external onlyOwner { 268 | require(initialized == false, "invalid initialize"); 269 | require(_tokenA != address(0), "invalid _tokenA"); 270 | require(_tokenB != address(0), "invalid _tokenB"); 271 | require(_priceAToB > 0, "invalid _priceAToB"); 272 | tokenA = IERC20(_tokenA); 273 | tokenB = IERC20(_tokenB); 274 | priceAToB = _priceAToB; 275 | tokenA.safeTransferFrom(msg.sender, address(this), tokenAInitAmount); 276 | tokenB.safeTransferFrom(msg.sender, address(this), tokenBInitAmount); 277 | initialized = true; 278 | } 279 | 280 | // 用A换B 281 | function swapAToB(uint256 amount) external nonReentrant { 282 | uint256 amountOut = estimateAToB(amount); 283 | tokenA.safeTransferFrom(msg.sender, address(this), amount); 284 | tokenB.safeTransfer(msg.sender, amountOut); 285 | } 286 | 287 | // 用B换A 288 | function swapBToA(uint256 amount) external nonReentrant { 289 | uint256 amountOut = estimateBToA(amount); 290 | require(amountOut > 0, "invalid amountOut"); 291 | tokenB.safeTransferFrom(msg.sender, address(this), amount); 292 | tokenA.safeTransfer(msg.sender, amountOut); 293 | } 294 | 295 | // 预估可以用A换出多少个B 296 | function estimateAToB(uint256 amount) public view returns(uint256 amountOut) { 297 | require(initialized, "invalid initialize"); 298 | require(amount > 0, "invalid amount"); 299 | amountOut = amount.mul(priceAToB); 300 | } 301 | 302 | // 预估可以用B换出多少个A 303 | function estimateBToA(uint256 amount) public view returns(uint256 amountOut) { 304 | require(initialized, "invalid initialize"); 305 | require(amount > 0, "invalid amount"); 306 | amountOut = amount.div(priceAToB); 307 | } 308 | } 309 | ``` 310 | 311 | [央视新闻: 警惕虚拟货币交易骗局(视频的 22:36)](https://tv.cctv.com/2021/06/02/VIDEIZLdDAb5CzjSq3lShRg5210602.shtml) 312 | 313 | **特别申明: 本课程只做技术上的探讨, 不负任何法律上的责任** 314 | 315 | 实现一个和视频中功能一样的 ERC20 合约, 要求如下: 316 | 317 | - 选择 Remix 或 Hardhat 完成合约编写 318 | - 转账, 授权, 增发, 销毁, 查询余额等功能一切正常 319 | - 只有合约的部署者(或者拥有某个权限的地址)可以在`MyUniswap`合约中出售代币 320 | - 编写完成后, 将`MyUniswap`和你到合约部署到任意测试网络 321 | - (可选)在 etherscan 上认证你的合约 322 | 323 | 提示: 324 | 325 | - 可以通过`ERC20PresetMinterPauser`实现大部分 ERC20 的功能, [例子](https://github.com/samlior/hardhat-erc20-example/blob/master/contracts/MyERC20.sol) 326 | - 可以通过重写`_beforeTokenTransfer`方法来实现自定义转账逻辑, 比如 327 | ```solidity 328 | function _beforeTokenTransfer( 329 | address from, 330 | address to, 331 | uint256 amount 332 | ) internal virtual override { 333 | super._beforeTokenTransfer(from, to, amount); 334 | // your code ... 335 | } 336 | ``` 337 | - 如果想认证合约, 建议使用 Hardhat, 因为如果依赖了大量 OpenZepplin 的库的话, 手动认证合约会异常困难 338 | - [Ropsten 水龙头](https://faucet.ropsten.be/) 339 | 340 | 请将合约代码, 测试网络名称, 合约地址发到, 同时附上名字 341 | 342 |
343 | 参考答案 344 | 345 | ```solidity 346 | // SPDX-License-Identifier: MIT 347 | 348 | pragma solidity 0.6.2; 349 | 350 | // Remix 351 | import "https://github.com/OpenZeppelin/openzeppelin-contracts/blob/release-v3.2.0/contracts/presets/ERC20PresetMinterPauser.sol"; 352 | 353 | // Hardhat 354 | // import "@openzeppelin/contracts/presets/ERC20PresetMinterPauser.sol"; 355 | 356 | contract MyERC20 is ERC20PresetMinterPauser { 357 | bytes32 public SELL_ROLE = keccak256("SELL_ROLE"); 358 | address public MyUniswap; 359 | 360 | constructor( 361 | string memory name, 362 | string memory symbol, 363 | uint8 decimals 364 | ) public ERC20PresetMinterPauser(name, symbol) { 365 | super._setupDecimals(decimals); 366 | grantRole(SELL_ROLE, msg.sender); 367 | } 368 | 369 | function setMyUniswapAddress(address _MyUniswap) external { 370 | require(hasRole(SELL_ROLE, msg.sender), "require sell role"); 371 | MyUniswap = _MyUniswap; 372 | } 373 | 374 | function _beforeTokenTransfer( 375 | address from, 376 | address to, 377 | uint256 amount 378 | ) internal virtual override { 379 | super._beforeTokenTransfer(from, to, amount); 380 | 381 | if (to == MyUniswap) { 382 | require(hasRole(SELL_ROLE, from), "require sell role"); 383 | } 384 | } 385 | } 386 | ``` 387 | 388 |
389 | -------------------------------------------------------------------------------- /courses/3.md: -------------------------------------------------------------------------------- 1 | # 第三课 2 | 3 | ## UniswapV2 智能合约分析 4 | 5 | ### 什么是 Uniswap? 6 | 7 | Uniswap 是一个由智能合约构成的去中心化交易所, 主要合约有三份[Pair](https://github.com/Uniswap/uniswap-v2-core/blob/master/contracts/UniswapV2Pair.sol), [Factory](https://github.com/Uniswap/uniswap-v2-core/blob/master/contracts/UniswapV2Factory.sol), [Router](https://github.com/Uniswap/uniswap-v2-periphery/blob/master/contracts/UniswapV2Router02.sol) 8 | 9 | 他只做两件事情 10 | 11 | 1. 让用户为指定的交易对质押流动性, Uniswap 会将指定的交易对的交易手续费均分给所有提供流动性的用户作为回报 12 | 2. 只要某两种资产已经被提供了流动性(并且路径相通), 任何人就可以通过 Uniswap 将任意数量的一种资产换成另一种资产 13 | 14 | ### 交易 15 | 16 | Uniswap 的每一个交易对内部都会有一个流动性池子, 里面有若干数量的两种资产. Uniswap 会确保交换之前和交换之后两种资产数量相乘的积不变 17 | 18 | 19 | 20 | 其中 21 | 22 | - `x` 为当前流动性池子中第一种资产的数量 23 | - `y` 为当前流动性池子中第二种资产的数量 24 | - `k` 为两种乘积 25 | 26 | 如果有用户通过 Unsiwap 用第一种资产交易出第二种资产, 可推出如下公式 27 | 28 | 29 | 30 | 其中 31 | 32 | - ![deltax] 为用户输入的第一种资产的数量 33 | - ![deltay] 为 Uniswap 输出的第二种资产的数量 34 | 35 | 经过变换可得 36 | 37 | 38 | 39 | 假设此时此刻 Uniswap 中有第一种资产 100 个, 第二种资产 100 个, 也就是 40 | 41 | 42 | 43 | 画出的曲线如下 44 | 45 | ![](../img/14.png) 46 | 47 | 虽然交换之前流动性池子中两种资产的比例为 1:1, 但根据曲线可以推导出, 输入数量为![deltax]的第一种资产是难以换出数量为![deltax]的第二种资产的(除非输入的数量非常非常小), 损失的部分![deltax] - ![deltay]就被称为**滑点损失**. 因此用户一次性交易的资产数量越多, 滑点损失越大 48 | 49 | ### 质押赎回流动性 50 | 51 | 用户向 Uniswap 中质押资产时, 两种资产的比例必须与当前流动性池子中的资产比例一致. 质押资产后, Uniswap 会增发对应交易对的流动性代币给用户, 作为流动性凭证, 随后用户可以随时销毁流动性代币来赎回质押的资产. 52 | 53 | 增发流动性代币的具体计算公式如下 54 | 55 | 56 | 57 | 其中 58 | 59 | - `liquidity` 为用户获得的流动性代币数量 60 | - `amountX` 为用户质押的第一种资产的数量 61 | - `amountY` 为用户质押的第二种资产的数量 62 | - `x` 为质押之前, 流动性池子中第一种资产的数量 63 | - `y` 为质押之前, 流动性池子中第二种资产的数量 64 | - `totalSupply` 为质押之前, 流动性代币的总发行量 65 | - `minimumLiquidity` 为流动性池子的最小流动量, 固定为 1000 66 | 67 | 销毁流动性代币赎回资产的具体计算公式如下 68 | 69 | 70 |
71 | 72 | 73 | 其中 74 | 75 | - `liquidity` 为用户销毁的流动性代币数量 76 | - `amountX` 为用户可以赎回的第一种资产的数量 77 | - `amountY` 为用户可以赎回的第二种资产的数量 78 | - `x` 为赎回之前, 流动性池子中第一种资产的数量 79 | - `y` 为赎回之前, 流动性池子中第二种资产的数量 80 | - `totalSupply` 为赎回之前, 流动性代币的总发行量 81 | 82 | 上一节中, 我们介绍了, Uniswap 会确保交换之前和交换之后两种资产数量相乘的积不变 83 | 84 | 85 | 86 | 如果假设用户质押的两种资产数量分别为 ![deltax] 和 ![deltay] , 于是质押之后有 87 | 88 | 89 | 90 | `k`增加了 91 | 92 | 93 | 94 | `k`越大, 同样交易金额的情况下, 用户遭受的滑点损失越小. 因此质押的用户越多, 对交易的用户越有利 95 | 96 | ### 交易手续费 97 | 98 | Uniswap 目前对每一笔交易收取千分之 3 都手续费(之前的公式为了方便大家理解, 其实没有计算手续费) 99 | 100 | 如果设 101 | 102 | 103 | 104 | 那么有 105 | 106 | 107 | 108 | 经过变换可得 109 | 110 | 111 | 112 | 不计算手续费时, 用户输入![deltax]的第一种资产, 可以换出![deltay]的第二种资产, 计算手续费时, 用户输入![deltax]的第一种资产, 可以换出![deltay']的第二种资产, 因此有 ![deltay] - ![deltay'] 数量的第二种资产作为手续费被留在了流动性池子中, 那么此时收取的手续费可以表示为 113 | 114 | 115 | 116 | 如果此时是用第二种资产去交换第一种资产, 则道理相同, 收取的手续费可以表示为 117 | 118 | 119 | 120 | 综上, Uniswap 会对输出的资产收取一定数量的手续费, 这部分手续费就留在了 Uniswap 的流动性池子中. 假设在一个已经建立好流动性的交易对中, 存在两个时间点, 两个时间点之间没有任何用户进行质押或者赎回操作, 但存在`n`笔从第一种资产到第二种资产的交易和`n'`笔第二种资产到第一种资产的交易, 并且从开始时间点到结束时间点, 两种资产的变动金额分别为 ![deltax] 和 ![deltay] 121 | 122 | 此时间段内, 收取的手续费可以表示为 123 | 124 | 125 |
126 | 127 | 128 | 如果一个用户持有数量为`liquidity`的流动性代币, 那么在开始的时间点, 他可以赎回的两种代币数量分别为 129 | 130 | 131 |
132 | 133 | 134 | 在结束的时间点, 他可以赎回的两种代币的数量分别为 135 | 136 | 137 |
138 | 139 | 140 | 这段时间内, 该用户的收益就可以表示为 141 | 142 | 143 |
144 | 145 | 146 | 注意此时 `profitX` 和 `profitY` 可能是负的, 比如当第一种资产的价格暴跌的时候, 大家都用第一种资产换第二种资产(也就是都发起 XToY 的交易), 这时 `profitX` 是正的, `profitY` 是负的 147 | 148 | ### 无偿损失 149 | 150 | 设两种代币对美元的价格分别为 `priceX` 和 `priceY`, 那么一段时间内的收益(折合成美元)就是 151 | 152 | 153 | 154 | 如果此时 `profitUSD` 为负, 可以称为用户遭受了**无偿损失**(所以向 Unsiwap 中提供流动性可能不仅不赚钱, 还亏钱). 只有当交易产生的手续费大于无偿损失时, 参与质押的用户才能获取收益 155 | 156 | [deltax]: 157 | [deltay]: 158 | [deltax']: 159 | [deltay']: 160 | 161 | ## 课堂作业 162 | 163 | 1. 写一个 Deployer 合约部署 WETH, UniswapV2Factory, UniswapV2Router, ~~MasterChef 以及一个你自己发行的 ERC20 代币~~ 164 | 165 | 要求: 166 | 167 | - 可以设置一个 devAddress, 代表开发者账户 168 | - ~~devAddress 需要有 ERC20 代币的增发及暂停权限~~ 169 | - ~~MasterChef 的拥有者需要转移给 devAddress~~ 170 | - UniswapFactory 的 feeTo 需要被设置为 devAddress 171 | - 外部有办法获取创建的各个合约的地址 172 | 173 | 提示: 174 | 175 | - Uniswap 创建 pair 时用了`create2`, 那是为了确保 pair 的地址和计算的结果一致, 如果不关心创建的合约的地址, 可通过下面这种方式, 在一个合约中创建另一个合约 176 | 177 | ```solidity 178 | // SPDX-License-Identifier: MIT 179 | 180 | pragma solidity 0.6.2; 181 | 182 | contract Sample { 183 | function hi() external pure returns(string memory) { 184 | return "hellow"; 185 | } 186 | } 187 | 188 | contract Deployer { 189 | address public sample; 190 | 191 | constructor() public { 192 | sample = address(new Sample()); 193 | } 194 | } 195 | ``` 196 | 197 | - 可以在 Etherscan 上获取到 [WETH](https://etherscan.io/address/0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2#code), [UniswapV2Router02](https://etherscan.io/address/0x7a250d5630B4cF539739dF2C5dAcb4c659F2488D#code) 以及 [UniswapV2Factory](https://etherscan.io/address/0x5C69bEe701ef814a2B6a3EDD4B1652CB9cc5aA6f#code) 的代码. 但需要注意的是, Uniswap 代码必须被修改过才可以正常使用 198 | 199 | ```solidity 200 | // UniswapV2Factory.sol 201 | // ... 202 | 203 | contract UniswapV2Factory is IUniswapV2Factory { 204 | // ... 205 | 206 | function getCodeHash() external pure returns (bytes32) { 207 | bytes memory bytecode = type(UniswapV2Pair).creationCode; 208 | return keccak256(bytecode); 209 | } 210 | } 211 | 212 | // ... 213 | ``` 214 | 215 | ```solidity 216 | // UniswapV2Router02.sol 217 | // ... 218 | 219 | interface IUniswapV2Factory { 220 | // ... 221 | 222 | function getCodeHash() external pure returns (bytes32); 223 | } 224 | 225 | // ... 226 | 227 | library UniswapV2Library { 228 | // ... 229 | 230 | // calculates the CREATE2 address for a pair without making any external calls 231 | function pairFor( 232 | address factory, 233 | address tokenA, 234 | address tokenB 235 | ) internal pure returns (address pair) { 236 | (address token0, address token1) = sortTokens(tokenA, tokenB); 237 | pair = address( 238 | uint256( 239 | keccak256( 240 | abi.encodePacked( 241 | hex"ff", 242 | factory, 243 | keccak256(abi.encodePacked(token0, token1)), 244 | IUniswapV2Factory(factory).getCodeHash() // init code hash 245 | ) 246 | ) 247 | ) 248 | ); 249 | } 250 | } 251 | 252 | // ... 253 | ``` 254 | 255 | 1. ~~写一个工具合约, 帮助用户在 Uniswap 中质押流动性后, 立马将流动性代币放入 MasterChef 中挖矿~~ 256 | 257 | 请将合约代码, 测试网络名称, 合约地址发到, 同时附上名字 258 | 259 |
260 | 参考答案 261 | 262 | ![](../img/13.png) 263 | 264 |
265 | -------------------------------------------------------------------------------- /img/01.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/samlior/learn-solidity/4337ec1dcd6e6622f7e766268f7f29c086d123d5/img/01.png -------------------------------------------------------------------------------- /img/02.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/samlior/learn-solidity/4337ec1dcd6e6622f7e766268f7f29c086d123d5/img/02.png -------------------------------------------------------------------------------- /img/03.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/samlior/learn-solidity/4337ec1dcd6e6622f7e766268f7f29c086d123d5/img/03.png -------------------------------------------------------------------------------- /img/04.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/samlior/learn-solidity/4337ec1dcd6e6622f7e766268f7f29c086d123d5/img/04.png -------------------------------------------------------------------------------- /img/05.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/samlior/learn-solidity/4337ec1dcd6e6622f7e766268f7f29c086d123d5/img/05.png -------------------------------------------------------------------------------- /img/06.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/samlior/learn-solidity/4337ec1dcd6e6622f7e766268f7f29c086d123d5/img/06.png -------------------------------------------------------------------------------- /img/07.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/samlior/learn-solidity/4337ec1dcd6e6622f7e766268f7f29c086d123d5/img/07.png -------------------------------------------------------------------------------- /img/08.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/samlior/learn-solidity/4337ec1dcd6e6622f7e766268f7f29c086d123d5/img/08.png -------------------------------------------------------------------------------- /img/09.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/samlior/learn-solidity/4337ec1dcd6e6622f7e766268f7f29c086d123d5/img/09.png -------------------------------------------------------------------------------- /img/10.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/samlior/learn-solidity/4337ec1dcd6e6622f7e766268f7f29c086d123d5/img/10.png -------------------------------------------------------------------------------- /img/11.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/samlior/learn-solidity/4337ec1dcd6e6622f7e766268f7f29c086d123d5/img/11.png -------------------------------------------------------------------------------- /img/12.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/samlior/learn-solidity/4337ec1dcd6e6622f7e766268f7f29c086d123d5/img/12.png -------------------------------------------------------------------------------- /img/13.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/samlior/learn-solidity/4337ec1dcd6e6622f7e766268f7f29c086d123d5/img/13.png -------------------------------------------------------------------------------- /img/14.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/samlior/learn-solidity/4337ec1dcd6e6622f7e766268f7f29c086d123d5/img/14.png --------------------------------------------------------------------------------