# Solidity ###### tags: `Blockchain` :::spoiler Reference - [[實戰篇] 訪問私有動態數據類型](https://mirror.xyz/hackbot.eth/oq6e37ApuACqsKGYUcnrTbz26k8P6TsGb-AHBq6_piY) - [深入 EVM 之合約的部署與調用](https://mirror.xyz/hackbot.eth/efwjEsiswbgcKlypI1BjIhda2I3nOoBLLOFDIsbX7mI?utm_source=substack&utm_medium=email) - [基礎智能合約語法](https://ithelp.ithome.com.tw/articles/10216370) - [WTF Academy](https://www.wtf.academy/) - [WTF Academy - Github](https://github.com/AmazingAng/WTF-Solidity) - [Learn Web3](https://learnweb3.io) - [Smart Contract 開發 - 使用 Solidity](https://ithelp.ithome.com.tw/users/20092025/ironman/1759?page=2) - [Solidity 進階之靜態分析](https://zhuanlan.zhihu.com/p/557960574) - [基礎智能合約語法](https://ithelp.ithome.com.tw/articles/10216370) - [Smart Contract Programmer - Youtube](https://www.youtube.com/channel/UCJWh7F3AFyQ_x01VKzr9eyA/featured) - [solidity 代碼功能模組](https://www.jianshu.com/p/c73320f73e00) - [Solidity 及 EVM 開發工具介紹](https://medium.com/taipei-ethereum-meetup/tools-for-debugging-solidity-and-evm-interaction-285f2bfb9c17) - [Alchemy University](https://university.alchemy.com/) - [103 Ethereum Solidity Developer Interview Questions](https://dzone.com/articles/how-to-pass-interview-on-solidity-part-1) - [The Ultimate Collection of Ethereum, Solidity and Smart Contracts Interview Questions](https://github.com/SoftZen/The-Ultimate-Collection-of-Ethereum-Solidity-and-Smart-Contracts-Interview-Questions/blob/master/README.md) - [Learn Solidity, Blockchain Development, & Smart Contracts | Powered By AI](https://youtu.be/umepbfKp5rI) - [Solidity by Example](https://solidity-by-example.org/) - [Initialize Hardhat Project](https://github.com/ewe-technology/pancakeswap-V3-strategy-contract/blob/main/%5BDoc%5D%20ProjectSetup.md) - [Smart Contract Vulnerabilities](https://github.com/kadenzipfel/smart-contract-vulnerabilities) - [Auditors Roadmap](https://github.com/razzorsec/AuditorsRoadmap/tree/main) - [30天 讓你從【零基礎】掌握智能合約 Solidity 入門走到飛](https://ithelp.ithome.com.tw/users/20125127/ironman/6066) - [Solidity by example](https://solidity-by-example.org/first-app/) - [Exploring Solidity Assembly: A Deep Dive into its Mechanism](https://www.linkedin.com/pulse/exploring-solidity-assembly-deep-dive-its-mechanism-soares-m-sc-/?trackingId=dVdzurHD6P7JgFYDJDx0GQ%3D%3D) ::: --- :::spoiler Enhance Develop - [Ethernaut](https://ethernaut.openzeppelin.com) - [Bitcoin Whitepaper](https://bitcoin.org/bitcoin.pdf) - [Ethereum Whitepaper](https://ethereum.org/en/whitepaper/) - [SWC Registry](https://swcregistry.io/) - [CryptoZombies](https://cryptozombies.io/) - [Immunefi](https://immunefi.com/) - [Audit Reports](https://solodit.xyz/) - [Solidity Documentation](https://docs.soliditylang.org/en/v0.8...) - [Smart contract audit skills roadmap](https://github.com/slowmist/SlowMist-Learning-Roadmap-for-Becoming-a-Smart-Contract-Auditor) ::: --- :::spoiler Tool - [Remix](https://remix.ethereum.org/?#optimize=false&runs=200&evmVersion=null&version=soljson-v0.8.7+commit.e28d00a7.js) - [Solidity Debugger Pro](https://marketplace.visualstudio.com/items?itemName=robertaachenw.solidity-debugger-pro) ::: ## Solidity 語法基礎:零基礎學習以太坊開發 - 認識 solidity - 區塊鏈 1.0 到 2.0、以太坊簡史 - 什麼是 Solidity、以太坊上的其他開發語言、選擇 Solidity 開發的理由 - EVM 與智能合約 - 以太幣與 Gas fee - 何謂 DApp 與知名 DApp 介紹 - Chapter 2 Solidity 開發基礎 - 線上開發工具介紹:Remix IDE - 從簡易 Solidity 範例看智能合約的優點、限制與迷思 - Solidity 拆解概覽 - 區塊鏈錢包: Metamask 介紹 - 驗證並查看智能合約:Etherscan 簡介 - Chapter 3 Solidity 進階實作 - Solidity 範例解析 - 常見的智能合約協議:ERC 介紹( ERC-20、ERC-721A、ERC-1155、ERC-721R ) - [實作]發行自己的代幣:ERC-20 與協議 - [實作]發行自己的 NFT:ERC-721A 與協議 https://courses.zinstitute.net/courses/web3frontend ## 認識 solidity ### 區塊鏈的 1.0、2.0、3.0 時代 :::info 區塊鏈的 1.0 ::: ![](https://i.imgur.com/qAXBbyd.png) 區塊鏈 1.0 主要是以數字貨幣 ![](https://i.imgur.com/LVMTxhQ.png) ![](https://i.imgur.com/jPkQGJT.png) :::info 區塊鏈的 2.0 ::: 區塊鏈 2.0 是指智能合約,智慧合約與貨幣相結合,對金融領域提供了更加廣泛的應用場景. 區塊鏈 2.0 的代表是'以太坊'.乙太坊是一個平臺,它提供了各種模組讓使用者用以搭建應用. 平臺之上的應用,其實也就是合約. 這是乙太坊技術的核心。 區塊鏈 2.0 架構 與 1.0 相比,支援智能合約 ![](https://i.imgur.com/L9NU6u2.png) :::info 區塊鏈 3.0 ::: 區塊鏈 3.0 是指區塊鏈在金融行業之外的各行業的應用場景.能夠滿足更加複雜的商業邏輯 ![](https://i.imgur.com/kMPz37D.png) 區塊鏈 3.0 架構 增加了閘道控制,增加對安全保密的需求支援和數據審計加強對數據的可靠性管理 ![](https://i.imgur.com/p0MAtdz.png) ### 什麼是 Solidity、以太坊上的其他開發語言、選擇 Solidity 開發的理由 Solidity 提供了一個龐大的群體和以太坊圖書館。這種網絡效應促進了合作,也增加了特定 dApp 獲得更多用途的可能性。 每種語言的工作需求量都在呈上升趨勢,而掌握多種語言的開發人員會有更多機會選擇工作。目前,對掌握 Solidity 的開發人員的需求非常大,但學習 Go、Haskell 或 Rust 會更具優勢,因為使用它們進行開發的程序員較少,同時這些語言也更受雇主青睞 ### 何謂 DApp 與知名 DApp 介紹 :::info 何謂 DApp ::: 去中心化應用( Decentralized Application, DApp )為建構於區塊鏈上的應用程式,也被稱之為分散式應用,DApp 建構於區塊鏈網路,DApp 與區塊鏈之間的關係,就像 App 建構上 iOS 和 Android 系統上,DApp 讓區塊鏈可以展開各種應用價值,可說是開啟了區塊鏈時代 如果工程師想開發一個 App,傳統的 App 必須要選擇 iOS 或 Android 系統,DApp 則必須開發在區塊鏈的公鏈系統上,所以兩者之間的差異在於系統不同,以及整體是否是中心化管理,在 App 中所有的使用者資料都會被開發者所掌控,並儲存在開發方的資料平台上,使用者則難以追溯 App 的開發方式及細節,但在 DApp 中,資料加密後儲存在區塊鏈上,使用者可從區塊鏈上了解所有的開發資料,擁有自己在 DApp 中所擁有的任何虛擬資產的掌控權 #### DApp 的三大特點 只要區塊鏈上開發的 APP 都是 DApp 嗎?答案是否定的,DApp 必須符合開源、利用代幣以及具有不變的演算法支持,才算是一個 DApp。 - 開源 ( Open Source ) - DApp 必須是完全開源 - 所有的數據都必須以加密方式,分散式儲存在公共去中心化區塊鏈上 - 代幣 ( Tokens ) - DApp 必須透過演算法產生原生的虛擬貨幣( Coin)或用智能合約產出代幣( Token ),並進一步利用作為獎勵或營運基礎,也可利用代幣獎勵挖礦機制。 - 算法 ( Algorithm ) - DApp 透過演算法生成代幣,代幣必須與應用程式所提供的服務相關,如果有任何貢獻或獎勵機制必須透過此演算法進行,演算法將公開於區塊鏈上且不能被修改。 - 例如比特幣根據演算法生成,而礦工則是為了獎勵對比特幣區塊鏈的貢獻,而獲得比特幣獎勵。 ## Solidity 開發基礎 Solidity 是一種合約式導向的程式語言,用來撰寫智能合約 Solidity 是靜態型語言,編譯後可以在 EVM 上執行。撰寫以太坊的智能合約,除了可以用 Solidity 語言,還有 Vyper 語言可以選擇 ### 基本的完整智能合約 合約程式碼的第一行一定是 pragma 開頭,它是用來告訴編譯器,如何編譯我們所撰寫的原始碼,^0.8.4 指的是最低可接受用 0.8.4 版來編譯 contract 是保留字,用法類似於其他程式語言的 class。除此之外,還有 view、payable、constant、pure 等都是保留字 ```solidity= pragma solidity ^0.8.4; contract SimpleStorage { uint storedData; function set(uint x) public { storedData = x; } // returns 是有加 s,並非筆誤。 function get() public view returns (uint) { return storedData; } } ``` ### 合約的結構 contract 語法類似於物件導向程式語言的 class,而且也可以使用一般繼承跟多重繼承 - 狀態變數 (State Variables) - 函式 (Functions) - 修飾函式 (Function Modifiers) - 事件 (Events) - 結構型別 (Struct) - 列舉型別 (Enum) --- - 狀態變數 (State Variables) : 狀態變數是用來在區塊鏈上保存值 ```solidity= pragma solidity ^0.4.25; contract Example { string message; // 狀態變數 } ``` - 函式 (Functions) : 函式可以被執行 ```solidity= pragma solidity ^0.4.25; contract Example { function hello() public {} } ``` - 修飾函式 (Function Modifiers) : - 使用情境: - 重複性的前置條件檢查 - 切割多個 require ```solidity= modifier name([argument, ...]) { ... _; ... } ``` - 事件 (Events) : DApp 經常聆聽智能合約的事件,來進行非同步頁面更新。 ```solidity= // 宣告 event FundTransfer(address indexed to, uint value); // 呼叫 emit FundTransfer(someAddress, 100); ``` 參數宣告若添加 indexed 則代表之後這個值,可以被使用來 filter,反之則無法被使用來 filter。 - 結構型別 (Struct) : 結構 (Struct) 可以將自訂的不同資料型態綁一起,使值更加結構化 ```solidity= pragma solidity ^0.4.25; contract Example { struct User { string name; uint age; uint height; uint weight; } } ``` - 列舉型別 (Enum) ```silidity= pragma solidity ^0.4.25; contract Contest { State state = State.Start; enum State { Start, Pending, End } function getState() view returns (uint) { return uint(state); } } ``` - 線上開發工具介紹:Remix IDE ![](https://i.imgur.com/dXJ62Rm.png) ### 型別 - Solidity中的變數類型 - 數值類型(Value Type):這類變數賦值時候直接傳遞數值 - bool : default is false - integer : 從 8 開始,以 8 遞增,例如 8, 16, 24, ... , 256。 - int : 有符號整數型別 - uint - 無符號整數型別 - 常用來表示貨幣數量和時間戳 (timestamp) - 值的範圍是 0 ~ 2 的 n 次方,以 uint8 來說 2^8-1。 - address - bytes( 位元組 ) : bytes1, bytes2, bytes3...bytes32 - 1個Byte是8 bits - 4個Bits指的是2的4次方,其可表示的數值範圍為0~15,恰恰好就是十六進位的表示範圍,因此每一個byte,會拆成以2個十六進位方式顯示。byte = 18 bytes ( 16 進位 ) - string : - 以 UTF8 編碼,動態分配大小 - Solidity 無法直接進行字串比對,可以用 sha3 來比對 ``` if (sha3(name1) == sha3(name2))``` - enum - 引用類型(Reference Type):包括數位和結構體,這類變數佔空間大,賦值時候直接傳遞位址(類似指標)。 - array : ```solidity= // 固定陣列 int[5] myArray; // 動態陣列 int[] anotherArray; - dynamic array // 宣告為記憶體陣列 uint[] memory myArray = new uint[](5); ``` - Struct - 映射類型(Mapping Type): 里的哈希表 - 函數類型(Function Type) : - function:聲明函數時的固定用法,想寫函數,就要以 function 關鍵字開頭。 - ```<function name>```:函數名。 - ```<parameter types>```:圓括弧裡寫函數的參數,也就是要輸入到函數的變數類型和名字。 - {internal | external | public| private}:函數可見性說明符,一共 4 種。 沒標明函數類型的,預設 internal - public: 內部外部均可見。 ( 也可用於修飾狀態變數,public 變數會自動生成 getter 函數,用於查詢數值 ). - private: 只能從本合約內部訪問,繼承的合約也不能用( 也可用於修飾狀態變數 )。 - external:只能從合約外部訪問( 但是可以用 ```this.f()f``` 來調用,是函數名 ) - internal: 只能從合約內部訪問,繼承的合約可以用( 也可用於修飾狀態變數 )。 - [pure | view| payable]:決定函數許可權/功能的關鍵字。 payable( 可支付的 )很好理解,帶著它的函數,運行的時候可以給合約轉入 ETH - [`returns ()`]:函數返回的變數類型和名稱 - Pure View : 約的狀態變數存儲在鏈上,很貴,如果不改變鏈上狀態,就不用付 - return vs returns : - returns 加在函數名後面,用於聲明返回的變數類型及變數名 - return 用於函數主體中,返回指定的變數 - 單位轉換 - 1 個位元組( bytes )是 8 位,二進位制 8 : xxxxxxxx 範圍從 00000000-11111111,表示 0 到 255 - 一位 16 進位制數( 用二進位制表示是 xxxx )最多隻表示到 15(即對應 16 進位制的 F),要表示到 255,就還需要第二位。 #### 在乙太坊中,以下語句被視為修改鏈上狀態: - 寫入狀態變數。 - 釋放事件。 - 創建其他合同。 - 使用.selfdestruct - 通過調用發送乙太幣 - 調用任何未標記或 view pure 的函數。 - 使用低級調用( low-level calls ) - 使用包含某些操作碼的內聯彙編 ### 變數初始值 #### 值類型初始值 - boolean : false - string : "" - int : 0 - uint : 0 - enum : 枚舉中的第一個元素 - address : 0x0000000000000000000000000000000000000000 ( 或 address ( 0 ) ) - function - internal : 空白方程 - external : 空白方程 可以用 public 變量的 getter 函數驗證上面寫的初始值是否正確: ```solidity= 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; // 第一個元素 0 function fi() internal{} // internal 空白方程 function fe() external{} // external 空白方程 ``` #### 引用類型初始值 - 映射 mapping: 所有元素都為其默認值的 mapping - 結構體 struct: 所有成員設為其默認值的結構體 - 數組 array - 動態數組: `[name=Sam]` - 靜態數組(定長): 所有成員設為其默認值的靜態數組 可以用 public 變量的 getter 函數驗證上面寫的初始值是否正確: ```solidity= // Reference Types uint[8] public _staticArray; // 所有成員設為其默認值的靜態數組[0,0,0,0,0,0,0,0] uint[] public _dynamicArray; // `[]` mapping(uint => address) public _mapping; // 所有元素都為其默認值的mapping // 所有成員設為其默認值的結構體 0, 0 struct Student{ uint256 id; uint256 score; } Student public student; ``` #### delete 操作符 delete a 會讓變量 a 的值變為初始值 ```solidity= // delete操作符 bool public _bool2 = true; function d() external { delete _bool2; // delete 會讓 _bool2 變為默認值,false } ``` ### 數據位置 solidity 數據存儲位置有三類: - storage:合約裡的狀態變數**預設都是 storage**,存儲在鏈上,類似計算機的硬碟,gas 消耗多 - memory:函數裡的參數和臨時變量一般用 memory,存儲在記憶體中,不上鏈,gas 消耗少 - calldata:和 memory 類似,存儲在記憶體中,不上鏈。 與 memory 的不同點在於 calldata 變數不能修改( immutable ),一般用於函數的參數,gas 消耗少 ```solidity! function fCalldata(uint[] calldata _x) public pure returns(uint[] calldata){ //参數为calldata數組,不能被修改 // _x[0] = 0 //這樣修改會報錯 return(_x); } ``` ### 變量的作用域 Solidity中變數按作用域劃分有三種,分別是狀態變數(state variable),局部變數(local variable)和全域變數(global variable) - Local : Declared inside a function and are not stored on blockchain - State : - Declared outside a function to maintain the state of the smart contract - Stored on the blockchain - Global - Provide information about the blockchain. They are injected by the Ethereum Virtual Machine during runtime. - Includes things like transaction sender, block timestamp, block hash, etc. --- #### State variable 狀態變數是數據存儲在鏈上的變數,所有合約內函數都可以訪問,gas 消耗高。狀態變數在合約內、函數外聲明: ```solidity= contract Variables { uint public x = 1; uint public y; string public z; } ``` 這樣就可以修改值 ```solidity= function foo() external{ // 可以在函數裡的值 x = 5; y = 2; z = "0xAA"; } ``` #### State variable 狀態變數是數據存儲在鏈上的變數,所有合約內函數都可以訪問,gas 消耗高。狀態變數在合約內、函數外聲明: ```solidity= contract Variables { uint public x = 1; uint public y; string public z; } ``` #### Local variable 全域變數是全域範圍工作的變數,都是 solidity 預留關鍵字。 他們可以在函數內不聲明直接使用: ```solidity= function global() external view returns(address, uint, bytes memory){ address sender = msg.sender; uint blockNum = block.number; bytes memory data = msg.data; return(sender, blockNum, data); } ``` ### 表達示與流程控制 輸出參數 可以返回多個數值,這點跟其他程式語言非常不同 ```solidity= pragma solidity ^0.4.16; contract Simple { function arithmetics(uint _a, uint _b) public pure returns (uint o_sum, uint o_product) { o_sum = _a + _b; o_product = _a * _b; } } ``` #### 函式與修飾標記 ```solidity= function name([argument, ...]) [visibility] [view|pure] [payable] [modifier, ...] [returns([argument, ...])]; ``` 可見度 (visibility) 有四種 public, private, internal, external - view : 當函式不修改任何狀態時,可以在函式宣告時標記 view 關鍵字 - 以下的情境會修改狀態: - 寫入或更改狀態變數的值 - 觸發事件 - 創建其他智能合約 (Creating other contracts.) - 呼叫 selfdestruct - 透過呼叫函式來發送 Ether - 呼叫其他不是 view 或 pure 的函式 - 使用 low-level 的呼叫 - **constant 是 view 的別名** - pure : 當函式不讀取或不修改狀態時,可以在函式宣告時標記 pure 關鍵字。 - 除了上面列表的情況之外,還要考慮以下情況: - 讀取狀態變數 - 存取 ```this.balance``` 或 ```<address>.balance.``` - 存取任何 block, tx, msg 物件裡的屬性 (除了 msg.sig 和 msg.data 是例外) - 呼叫任何沒有標記 pure 的函式 - payable : 讓函式可以接收以太幣 - fallback : - 合約可以有唯一個沒有函式名稱的函式,此函式不沒有參數,也不能返回值。如果在呼叫合約時,呼叫的函式名稱沒有比對到任何具名函式,這個函式就會被觸發。 - 除此之外,如果有人對於合約地址進行一般的轉帳作業,也會執行此函式。但為了要接收 Ether,函式必須要標記 payable,否則會無法接收 Ether。 - 通常呼叫 fallback 函式,只會消耗掉少量的 gas (例如 2300 gas),所以要盡量降低呼叫 fallback 函式的費用是非常重要的。 - 特別是以下操作,將消耗更多的 gas: - 寫入值 - 建立一個合約 (Creating a contract) - 呼叫會消耗大量 gas 的外部函式 - 發送 Ether - 在部署你的合約之前,請測試你的 fallback 函式,確保它執行費用低於 2300 gas。 - 雖然 fallback 函式規定不能有參數,但是你還是可以透過 msg.data 來取得資料。 - 如果你沒有宣告 fallback 函式,當有 EOA 帳戶轉錢給智能合約地址時,會直接直接拋出異常,並退回 Ether。 - 重載函式 (overloading) : 只要**參數數量不同**,合約可以有多個同名函式 ```solidity= pragma solidity ^0.4.16; contract A { function f(uint _in) public pure returns (uint out) { out = 1; } function f(uint _in, bytes32 _key) public pure returns (uint out) { out = 2; } } ``` ### 錯誤處理 (Error handling) solidity 三種拋出異常的方法 error, require & assert - Error - error是 solidity 0.8 新加的內容,方便且高效(省 gas )地向用戶解釋操作失敗的原因 - 在執行當中,error 必須搭配 revert (回退)命令使用 ```solidity= error TransferNotOwner(); // 自定義 error function transferOwner1(uint256 tokenId, address newOwner) public { if(_owners[tokenId] != msg.sender){ revert TransferNotOwner(); } _owners[tokenId] = newOwner; } ``` - Require - 使用針對外部傳入值驗證,可針對條件判斷,當發生 false 時,會傳回剩餘未使用的 gas,回復所有狀態,情境常用於前置條件檢查 - require命令是 solidity v0.8 之前拋出異常的常用方法,目前很多主流合約仍然還在使用它。它很好用,唯一的缺點就是 **gas 隨著描述異常的字串長度增加,比 error 命令要高** - require(檢查条件,"異常的描述") ```solidity= function transferOwner2(uint256 tokenId, address newOwner) public { require(_owners[tokenId] == msg.sender, "Transfer Not Owner"); _owners[tokenId] = newOwner; } ``` - assert - 使用於內部狀態檢查,發生例外會傳回剩餘未使用的 gas,回復所有狀態,常用於驗證輸入值的邊界條件檢查 - assert 命令一般用於寫程式 debug ,因為它不能解釋拋出異常的原因(比 require 少個字串) - assert(檢查條件) ```solidity= function transferOwner3(uint256 tokenId, address newOwner) public { assert(_owners[tokenId] == msg.sender); _owners[tokenId] = newOwner; } ``` - 使用情境判斷 - require() 應該是你去檢查條件的函式,assert() 只用在防止永遠不應該發生的情境。至於 require() 與 revert() 使用的差別,只在於你需不需要條件是判斷 - 三種方法的gas比較 - require > assert > error - 因此,error 既可以告知用戶拋出異常的原因,gas 又能省,要多用! ### mapping 型別 你可以把 mapping 型別看做類似是一個 hash tables,它會虛擬初始化每一個 key 的值都預設為 0。但實際上 mapping 型別,並不是存 key 和 value 的真正的內容,而是 key 和 value 的 keccak256 hash ![](https://i.imgur.com/MUmeUhl.png) mapping 是一個動態大小的陣列,_KeyType 除了 enum 和 struct 型別不能使用之外,其他的型別都支援。_ValueType 支援所有的型別,包含 mapping 型別。 語法 ```solidity! mapping(_KeyType => _ValueType) ``` - 規則 - 映射的` _keyType` 只能選擇 solidity 預設的類型,比如 uint, address 等,不能用自定義的結構體,`_valueType` 而可以使用自定義的類型 ```solidity= // 定義一個結構體 Struct struct Student{ uint256 id; uint256 score; } mapping(uint => Student) public testVar; ``` - 映射的存儲位置必須是 storage ,因此可以用於合約的狀態變量、函數中的 storage 變量和 library 函數的參數。不能用於函數的參數或返回結果中,因為 mapping 記錄的是一種關係 ( key - value pair ) - 如果映射聲明為 public ,那麼 solidity 會自動給你創建一個 getter 函數,可以通過 key 來查詢對應的 value - 給映射新增的鍵值對的語法為 `_Var[Key] = _Value`,其中 `_Var` 是映射變數名,`_Key` 和 `_Var` 對應新增的鍵值對 ```solidity= function writeMap (uint _Key, address _Value) public{ idToAddress[_Key] = _Value; } ``` - 映射的原理 - 原理 1: 映射不儲存任何鍵( Key )的資訊,也沒有 length 的資訊 - 原理 2: 映射使用 keccak256(key) 當成 offset 存取 value - 原理 3: 因為 Ethereum 會定義所有未使用的空間為 0,所以未賦值( Value )的鍵(Key)初始值都是 0 ### 可見度和自動生成 getter 函示 Solidity 有兩種呼叫函式的方式,一是呼叫內部函式,二是呼叫外部函式。函式和狀態變數則有四種可見度(Visibility)。 函式可以指定為 external、public、internal 或 private,如果沒有指定預設為 public。狀態變數不可指定為 external,預設是 internal。 - 外部 (external):外部函式是合約介面的一部份,它可以被其他合約呼叫,但不能被內部呼叫,除非使用 this 語法。適合使用在接收大量資料 (large arrays of data) 的時候。 - 公開 (public):可接受內部呼叫或外部呼叫,會自動生成公開狀態的 getter 函式。 - 內部 (internal):指定為 internal 的函式和狀態變數,只能在內部被存取,而且不需要使用 this 語法。 - 私有 (private):私有函式和私有狀態變數的可見度範圍,僅在於宣告時所在的合約。 ### 常數 constant 和 immutable 狀態變數聲明這個兩個關鍵字之後,不能在合約后更改數值 ; 並且還可以節省 gas 另外 1. 只有**數值變數**可以聲明和 constant 和 immutable 2. string 和 bytes 可以聲明為 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 變數**可以在聲明時或構造函數中初始化**,因此更加靈活。 ```solidity // 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` or 自定義的函數給 immutable 變量初始化 ```solidity= // 利用constructor初始化immutable變量,因此可以利用 constructor(){ IMMUTABLE_ADDRESS = address(this); IMMUTABLE_BLOCK = block.number; IMMUTABLE_TEST = test(); } function test() public pure returns(uint256){ uint256 what = 9; return(what); } ``` 常量( constant )和不變量( immutable ),讓不應該變數保持不變。這樣的做法能在節省 gas 的同時提升合約的安全性 ### 單位和全域變數 - 貨幣單位 (Ether Units) - 在數字的後面加上 wei、finney、szabo 或 ether 轉換為貨幣的單位,如果沒有指定,基礎單位就是 wei。 - 時間單位 (Time Units) - 在數字的後面加上 seconds、minutes、hours、days、weeks、years 轉換為時間單位,如果沒有指定,基礎單位就是秒 - 請小心的使用時間單位,因為有閏秒 (leap seconds),由於無法預測什麼時候發生閏秒,如果你需要使用精確的時間,需要透過外部函式庫來取得。由於這個原因,years 被棄用了 - 特殊變數和函式 - 區塊 (block) - `block.gaslimit (uint)`: 目前區塊的 gas limit - `block.number (uint)`: 目前區塊的編號 - `block.blockhash(uint blockNumber)`:區塊的 hash 值,只能用在最近的 256 各區塊,0.4.22 版之後被 blockhash(uint blockNumber) 取代。 - `block.coinbase (address)`: 目前的區塊礦工的位址 - `block.difficulty (uint)`: 目前的區塊難度 - `block.timestamp (uint)`: 目前區塊的時間戳 - msg - `msg.gasleft (uint)`: 剩餘的 gas - `msg.sender (address)`: 發送訊息給函式的位址 - `msg.data (bytes)`: 完整的 calldata - `msg.sig (bytes4)`: calldata 的前四個 bytes (例如 function identifier) - `msg.value (uint)`: 發送多少以太幣(單位是 wei) - transaction - `tx.gasprice (uint)`: 交易的 gas price - `tx.origin (address)`: 交易的發送者 - 其他 - `this`: 目前的合約 - `now`: 目前時間,是 block.timestamp 的暱稱。 - `gasleft() returns (uint256)`: 剩餘 gas - `keccak256([argument, ...])` - `sha256([argument, ...])` - `ripemd160([argument, ...])` - 不要倚賴 block.timestamp、now 或 blockhash 當作隨機亂數的來源,除非你知道你在做什麼。區塊的時間戳和 hash 都某種程度受到礦工的影響,如果你拿這兩個值作為博弈應用,可能會被社群中居心不良的礦工破解。 - 錯誤處理 (Error Handling) - `assert(bool condition)`:如果條件不滿足,transaction 會失敗,使用於內部錯誤。 - `require(bool condition)`:如果條件不符合,狀態會被還原,使用於輸入值得錯誤或外部元件。 - `require(bool condition, string message)`:同上,但可以自訂的錯誤訊息。 - `revert()`:執行會被終止,並回復到修改前的狀態。 - `revert(string reason)`:同上,但可以自訂錯誤訊息。 - 數學和加密函式 - `addmod(uint x, uint y, uint k) returns (uint)`:加法 - `mulmod(uint x, uint y, uint k) returns (uint)`:乘法 - `keccak256(...) returns (bytes32)`:計算傳入值的 Ethereum-SHA-3 (Keccak-256) hash,使用 tightly packed。 - `sha256(...) returns (bytes32)`:計算傳入值的 SHA-256 hash,使用 tightly packed。 - `sha3(...) returns (bytes32)`:keccak256 的別名 - `ripemd160(...) returns (bytes20)`:計算傳入值的 RIPEMD-160 hash,使用 tightly packed。(tightly packed 的意思是,計算時,會自動去掉空白 (padding)。) - `ecrecover(bytes32 hash, uint8 v, bytes32 r, bytes32 s) returns (address)` - 位址相關 - `<address>.balance (uint256)`:餘額,值得單位是 wei。 - `<address>.transfer(uint256 amount)`:轉錢給指定位址,值的單位是 wei,如果失敗會拋出錯誤訊息,需花費 2300 gas,gas 的設定不能調整。 - 合約相關 - `this`:指當前的合約,可轉換為合約的地址。 - `selfdestruct(address recipient)`:銷毀目前合約,並把合約裡的錢轉到指定的位址 ### 繼承、抽象合約 - 簡單繼承 - 規則 - virtual: 父合約中的函數,如果希望子合約重寫,需要加上 virtual 關鍵字 - override:子合約重寫了父合約中的函數,需要加上 override 關鍵字 - 多重繼承 - 規則 : ==繼承時要按輩分最高到最低的順序排==,比如我們寫一個 Erzi 合約,繼承 Yeye 合約和 Baba 合約,那麼就要寫成 contract Erzi is Yeye, Baba,而不能寫成 contract Erzi is Baba, Yeye,不然就會報錯。如果某一個函數在多個繼承的合約裡都存在,比如例子中的`hip()`和`pop()`,在子合約裡必須重寫,不然會報錯。重寫在多個父合約中都重名的函數時,override 關鍵字後面要加上所有父合約名字 ```solidity= 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) { emit Log("Erzi"); } ``` - 修飾器的繼承 - Solidity 中的修飾器(Modifier)同樣可以繼承,用法與函數繼承類似,在相應的地方加 virtual 和 override 關鍵字即可 - 構造函數的繼承 - 子合約有兩種方法繼承父合約的構造函數。舉個簡單的例子,父合約 A 裡面有一個狀態變量 a,並由構造函數的參數來確定: ```solidity= // 構造函數的繼承 abstract contract A { uint public a; constructor(uint _a) { a = _a; } } ``` 1. 在繼承時聲明父構造函數的參數,例如:contract B is A(1) 2. 在子合約的構造函數中聲明構造函數的參數,例如: ```solidity= contract C is A { constructor(uint _c) A(_c * _c) {} } ``` - 調用父合約的函數 - 子合約有兩種方式調用父合約的函數,直接調用和利用關鍵字 super - 直接調用:子合約可以直接用`父合約名.函數名`的方式來調用父合約函數 - super 關鍵字:子合約可以利用來調用最近的父合約函數,solidity 繼承關係按聲明時從右到左的順序是:contract Erzi is Yeye, Baba,那麼Baba是最近的父合約,`super.pop()`將調用`Baba.pop()`而不是`Yeye.pop()` - 繼承 (Inheritance) - Solidity 透過複製程式碼和多型 ( polymorphism ),來支援多重繼承 - 當一個合約繼承多個合約,實際上只會有一個合約被創建,合約會將所有繼承的合約的程式碼,複製到自己身上。 ### 抽象合約 ( Abstract Contracts ) 如果一個智能合約里至少有一個未實現的函數,即某個函數缺少主體 {} 中的內容,則必須將該合約標為 abstract,不然編譯會報錯;另外,未實現的函數需要加 virtual,以便子合約重寫 ```solidity= abstract contract InsertionSort{ function insertionSort(uint[] memory a) public pure virtual returns(uint[] memory); } ``` - 當合約被標註為抽象合約時,代表的是至少有一個函示為實現 - 如果合約繼承抽象合約後,並沒有實現所有未實現的函示,那麼自己也會是一個抽象合約 - 把合約標為 abstract 就為抽象合約 - 如果一個智能合約里至少有一個未實現的函數,即某個函數缺少主體中的內容,則必須將該合約標為 abstract ,不然編譯會報錯; 另外,未實現的函數需要加 virtual ,以便子合約重寫 ```solidity= abstract contract InsertionSort{ function insertionSort(uint[] memory a) public pure virtual returns(uint[] memory); } ``` - 沒有實現的函示與 Function Type 不同,即使它們的語法看起來很相似。 - 沒有實現的函示範例 (a function declaration): ```solidity! function foo(address) external returns (address); ``` - 函示類型 (Function Type) 的範例: ```solidity! function(address) external returns (address) foo; ``` ### 介面( 接口 )(interface) 介面與抽象合約相似,但它不能實作任何功能,還有以下限制: - 介面不能繼承其他合約或介面 - 介面不能定義建構子 ( constructor ) - 介面不能定義變數 ( variable ) - 介面不能定義結構型別 ( struct ) - 介面不能定義列舉型別 ( enum ) - 所有函數都必須是 external 且不能有函數體 - 繼承接口的合約必須實現接口定義的所有功能 合約可以繼承介面 ( interface ),也可以繼承其他合約。介面常用於定義標準,例如 ERC-20 是定義代幣 ( Token ) 標準 雖然介面不實現任何功能,但它非常重要。 介面是智慧合約的骨架,定義了合約的功能以及如何觸發它們:如果智慧合約實現了某種介面( 比如 ERC20 或 ERC721 ),其他 Dapps 和智慧合約就知道如何與它交互。 因為介面提供了兩個重要的資訊: 1. 合約裡每個函數的 bytes4 選擇器,以及基於它們的函數簽名 `函数名(每个參數類型)` 2. 介面 id(更多資訊見 EIP165) 另外,介面與合約 ABI( Application Binary Interface )等價,可以相互轉換:編譯介面可以得到合約的 ABI ,利用 [abi-to-sol](https://gnidan.github.io/abi-to-sol/) 工具也可以將 ABI json 文件轉換為 接口 sol 文件 介面和常規合約的區別在於每個函數都以 ; 代替函數體 `{}` 結尾 ### 函式庫 library 語法的使用方式類似 contract 語法,沒有自己的合約帳戶,所以在 library 不能使用 payable,也沒有 fallback 函式。 函式庫不需要實例化 ### Solidity 代碼檢查工具 合約的安全性自動化檢測有靜態分析、動態分析和形式化驗證。 - 靜態分析不執行合約代碼,通過對合約代碼做模式匹配或者語義分析來檢測漏洞 - 動態分析需要執行合約,通過大量的模糊測試來觀察合約的狀態是否會出現問題 - 形式化驗證是將合約的業務邏輯用數學表達式來描述,只要證明數學表達式是正確的,則合約的業務邏輯也是正確的(不代表合約的實現沒有問題) 靜態分析的優點是使用簡單,速度快,但只能檢測已知的安全漏洞。 動態分析能檢測出未知的安全問題,但是成本高、速度慢。 形式化驗證的使用範圍窄,比較適用於一些公共庫合約。 開發者對合約做靜態分析是最基本的要求,使用靜態分析工具可以快速檢測是否存在一些常見的漏洞,比如: - 許可權缺失,比如 Oracle 的更新沒有設置許可權 - 重入,這個出的問題最多 - 整數溢出 - DDOS,攻擊或者缺陷會導致合約無法執行正常的業務邏輯 - 價格操縱 但靜態分析工具不能檢測出跟業務邏輯特定相關的問題,還需要開發人員通過自檢去做人工靜態分析。 Tool - Solhint - Semgrep - Slither 自檢 合約的業務邏輯都是主要在介面中實現的,因此介面檢查就很重要: - 參數是否有校驗,尤其是需要注意是否有任意輸入。 - 介面必須是 external 或者 public 嗎? 如果把一個 internal 或者 private 介面暴露出去會非常危險。 - 需要加 payable 嗎? 不加的話沒法接收 eth,但是若無必要則一定不要加。 - 介面會修改狀態變數嗎? 修改這些變數需要許可權嗎? 這一點往往是靜態分析工具無法檢測到的漏洞。 - 通過 call 或者 delegatecall 的調用物件是可信的嗎? - 外部調用需要設置 gasLimit 嗎? 外表調用的返回結果需要處理嗎? - 在有外部調用的代碼前後遵守了 Checks-Effects-Interactions 規範嗎? - 外部調用能重入到合約中的其它介面然後通過旁路回到本介面嗎? - 如果介面涉及到 Token 的轉移,則需要的檢查有: 如果 Token 轉移過程中內部扣費會影響業務邏輯嗎? - Token 轉移過程中會有鉤子函數回調發送者或者接收者嗎? - Token 如果是可升級合約,對業務邏輯有影響嗎? - 有使用位址的 eth 餘額參與控制邏輯嗎? eth 的餘額是可以通過挖礦或者 selfdestruct 強制增加的。 對於借貸相關的合約,一般需要使用價格,則需要的檢查有: - Offchain oracle 是可靠的嗎? - Onchain oracle 的價格容易被操縱嗎? - LP token 的價格計算演算法是正確的嗎? 總之,所有的檢查都圍繞幾個核心: - 敏感的許可權是否能被轉移到任意位址 - 資產是否有可能被較小的代價轉走 - 資產是否有可能無法取出 靜態分析是相對容易掌握的工具,對開發複雜的Defi應用非常有説明。 不同的靜態分析工具可以結合使用,可以先使用 Solhint 來規範代碼,然後使用 Semgrep 來識別已知的漏洞,接著使用 Slither 來識別一些語義級別的問題。 ### 工具函式庫 - [Basic string utilities for Solidity](https://github.com/Arachnid/solidity-stringutils) - [Open Zeppelin solidity](https://github.com/OpenZeppelin/openzeppelin-contracts) ### Tool - [Remix](https://remix.ethereum.org) ## Solidity 進階 ### 函數重載( function overloading ) #### 重載 solidity 中允許函數進行重載,即==名字相同但輸入參數類型不同的函數可以同時存在==,他們被視為不同的函數 :::warning 注意,solidity 不允許修飾器( modifier )重載 ::: ```solidity= function saySomething() public pure returns(string memory){ return("Nothing"); } function saySomething(string memory something) public pure returns(string memory){ return(something); } ``` #### 實參匹配( Argument Matching ) 在調用重載函數時,會把輸入的實際參數和函數參數的變量類型做匹配。如果出現多個匹配的重載函數,則會報錯 ```solidity= // SPDX-License-Identifier: GPL-3.0 pragma solidity >=0.7.0 <0.9.0; contract overloading{ function saySomething() public pure returns(string memory){ return("Nothing"); } function saySomething(string memory something) public pure returns(string memory){ return(something); } function callSaySomething() external { saySomething('WTF'); } function f(uint8 _in) public pure returns (uint8 out) { out = _in; } function f(uint256 _in) public pure returns (uint256 out) { out = _in; } function callF() external { f(50); } } ``` ![](https://hackmd.io/_uploads/Hy_e5H0gp.png) #### 底層邏輯 - [keccak 256](https://emn178.github.io/online-tools/keccak_256.html) 可以用上面的工具去驗證 當輸入 ``saySomething()`` 會得到 `fbd61553bc5741fcbcf6707d585ab930767e670841d04e98f0dc3d6d75c6a246` 當輸入 ``saySomething(string)`` 會得到 `fe6b3783f56b1f3d3a231b407eaa4e8f7e970cd1106cdab4f7434c6347d4afc4` 可以看到前 8 碼是不一樣的 ### 庫合約 ( Library ) #### 庫函數 庫函數是一種特殊的合約,為了提升 solidity 代碼的複用性和減少 gas 而存在 他和普通合約主要有以下幾點不同: 1. 不能存在狀態變量 2. 不能夠繼承或被繼承 3. 不能接收以太幣 4. 不可以被銷毀 #### String 庫合約 String 庫合約是將 uint256 類型轉換為相應的 string 類型的代碼庫 ```solidity= 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) { if (value == 0) { return "0x00"; } uint256 temp = value; uint256 length = 0; while (temp != 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"); return string(buffer); } } ``` 他主要包含兩個函數,`toString()`將 uint256 轉為 string,`toHexString()`將 uint256 轉換為 16 進制,再轉換為 string #### 如何使用庫合約 用 String 庫函數的 `toHexString()` 來演示兩種使用庫合約中函數的辦法 1. 利用使用 for指令 指令 `using A for B;` 可用於附加庫函數(從庫 A )到任何類型( B )。添加完指令後,庫 A 中的函數會自動添加為 B 類型變量的成員,可以直接調用。注意:在調用的時候,這個變量會被當作第一個參數傳遞給函數 ```solidity= // 利用using for指令 using Strings for uint256; function getString1(uint256 _number) public pure returns(string memory){ // 庫函數会自動添加為 uint256 型變量的成员 return _number.toHexString(); } ``` 2. 通過庫合約名稱調用庫函數 ```solidity= // 直接通過庫合约名調用 function getString2(uint256 _number) public pure returns(string memory){ return Strings.toHexString(_number); } ``` - 常用庫合約 - 字串:將轉換為 uint256String - Address:判斷某個位址是否為合約位址 - Create2:更安全的使用 Create2 EVM opcode - Arrays:跟陣列相關的庫函數 ### Import - 通過源檔相對位置導入,例子: ``` 文件結構 ├── Import.sol └── Yeye.sol // 通过文件相對位置import import './Yeye.sol'; ``` - 通過源檔網址導入網上的合約,例子: ```solidity! import 'https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/utils/Address.sol'; ``` - 通過的目錄匯入,例子:npm ```solidity! import '@openzeppelin/contracts/access/Ownable.sol'; ``` - 通過導入特定的合約,例子:全局符號 ```solidity! import {Yeye} from './Yeye.sol'; ``` - 引用 `import()` 在代碼中的位置為:在聲明版本號之後,在其餘代碼之前。 ```solidity= // SPDX-License-Identifier: GPL-3.0 pragma solidity >=0.7.0 <0.9.0; contract Call{ event CallImport(string); function callImport() external { emit CallImport('Import'); } } ``` ```solidity= // SPDX-License-Identifier: GPL-3.0 pragma solidity >=0.7.0 <0.9.0; //import "./Call.sol"; import {Call} from "./Call.sol"; import * as Call from "./Call.sol"; //import {Call as CallToHell} from "./Call.sol"; contract HelloWorld{ function callCall() external { Call call = new Call(); call.callImport(); } } ``` ![](https://hackmd.io/_uploads/Bk_EhgWZp.png) #### Import struct & function ```solidity= // SPDX-License-Identifier: GPL-3.0 pragma solidity >=0.7.0 <0.9.0; struct Calls{ bool call; } function sayCall() pure returns(string memory){ return "Call"; } contract Call{ event CallImport(string); function callImport() external { emit CallImport('Import'); } } ``` ```solidity= // SPDX-License-Identifier: GPL-3.0 pragma solidity >=0.7.0 <0.9.0; //import "./Call.sol"; import {Call, Calls, sayCall} from "./Call.sol"; contract HelloWorld{ function callCall() external { Call call = new Call(); call.callImport(); } function callSayCall() external pure returns (string memory) { return sayCall(); } function callStructCall() external pure returns (bool) { Calls memory call = Calls(true); return call.call; } } ``` ![](https://hackmd.io/_uploads/HJPYbGZbp.png) ### 接收 & 發送 ETH ( Send & Transfer ) - 接收 Solidity 支援兩種特殊的回調函數,`receive()` 和 `fallback()`,他們主要在兩種情況下被使用: 1. 接收 ETH 2. 處理合約中不存在的函數調用(代理合約 proxy contract ) - 接收 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 執行更複雜的邏輯 ```solidity= // 定義事件 event Received(address Sender, uint Value); // 接收 ETH 時釋放 Received 事件 receive() external payable { emit Received(msg.sender, msg.value); } ``` 有些惡意合約,會在 `receive()` 函數(老版本的話,就是 `fallback()` 函數)嵌入惡意消耗 gas 的內容或者使得執行故意失敗的代碼,導致一些包含退款和轉帳邏輯的合約不能正常工作,因此寫包含退款等邏輯的合約時候,一定要注意這種情況。 - 回退函數 fallback `fallback()`函數會在調用合約不存在的函數時被觸發。可用於接收 ETH ,也可以用於代理合約 proxy contract。 `fallback()`聲明時不需要 function 關鍵字,必須由 external 修飾,一般也會用 payable 修飾,用於接收 ETH :`fallback() external payable { ... }`。 我們定義一個`fallback()`函數,被觸發時候會釋放 fallbackCalled 事件,並輸出 `msg.sender`, `msg.value` & `msg.data`: ```solidity= // fallback fallback() external payable{ emit fallbackCalled(msg.sender, msg.value, msg.data); } ``` - receive 和 fallback 的區別 ``` 觸發 fallback() 還是 receive()? 接收ETH | msg.data是空? / \ 是 否 / \ receive()存在? fallback() / \ 是 否 / \ receive() fallback() ``` 合約接收 ETH 時,`msg.data`為空且存在`receive()` 時,會觸發`receive()`;`msg.data`不為空或不存在`receive()`時,會觸發`fallback()`,此時f`allback()`必須為`payable`。 `receive()`和`payable fallback()`均不存在的時候,向合約發送 ETH 將會報錯 - 發送 Solidity 有三種方法向其他合約發送 ETH - `transfer()` - `send()` - `call()`,其中`call()`是被鼓勵的用法 ```solidity= // SPDX-License-Identifier: GPL-3.0 pragma solidity >=0.7.0 <0.9.0; contract ReceiveEvent{ event Receive(uint256 amount); event Fallback(uint256 amount); receive() external payable { emit Receive(msg.value); } fallback() external payable{ emit Fallback(msg.value); } } ``` #### transfer - 用法是接`收方地址.transfer`(發送 ETH 數額) - `transfer()`的 gas 限制是 2300 ,足夠用於轉賬,但對方合約的`fallback()`或`receive()`函數不能實現太複雜的邏輯 - `transfer()`如果轉帳失敗,會自動 revert(回滾交易) ```solidity= // 用 transfer() 發送 ETH function transferETH(address payable _to, uint256 amount) external payable{ _to.transfer(amount); } ``` #### send - 用法是`接收方地址.send`(發送 ETH 數額) - `send()`的 gas 限制是 2300,足夠用於轉賬,但對方合約的`fallback()`或`receive()`函數不能實現太複雜的邏輯。 - `send()`如果轉帳失敗,不會 revert。 - `send()`的返回值是 bool,代表著轉帳成功或失敗,需要額外代碼處理一下。 ```solidity= // send() 發送 ETH function sendETH(address payable _to, uint256 amount) external payable{ // 處理下 send 的返回值,如果失敗,revert 交易並發送 error bool success = _to.send(amount); if(!success){ revert SendFailed(); } } ``` #### call - 用法是`接收方地址.call{value: 發送ETH數額}("")` - `call()`沒有 gas 限制,可以支持對方合約`fallback()`或`receive()`函數實現複雜邏輯。 - `call()`如果轉帳失敗,不會 revert。 - `call()`的返回值是 (bool, data),其中 bool 代表著轉賬成功或失敗,需要額外代碼處理一下。 ```solidity= // call()發送ETH function callETH(address payable _to, uint256 amount) external payable{ // 處理下call的返回值,如果失敗,revert交易並發送error (bool success,) = _to.call{value: amount}(""); if(!success){ revert CallFailed(); } } ``` #### 整體比較 - call 沒有 gas 限制,最為靈活,是最提倡的方法 - transfer 有 2300 gas 限制,但是發送失敗會自動 revert 交易,是次優選擇 - send 有 2300 gas 限制,而且發送失敗不會自動 revert 交易,幾乎沒有人用它。 ### 調用其他合約 開發者寫智能合約來調用其他合約,這讓以太坊網絡上的程序可以復用,從而建立繁榮的生態。很多 web3 項目依賴於調用其他合約,比如收益農場(yield farming) ```solidity= // SPDX-License-Identifier: GPL-3.0 pragma solidity >=0.7.0 <0.9.0; contract OtherContract { uint256 private _x = 0; // 狀態變數 _x // 收到 eth 的事件,記錄 amount 和 gas event Log(uint amount, uint gas); // 返回合約 ETH 餘額 function getBalance() view public returns(uint) { 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; } } contract CallOtherContract{ function callSetX(address _Address, uint256 x) external{ OtherContract(_Address).setX(x); } function callSetX1(OtherContract _Address, uint256 x) external payable { _Address.setX(x); } function callSetX2(address _Address, uint256 x) external payable{ OtherContract oc = OtherContract(_Address); oc.setX(x); } function setXTransferETH(address otherContract, uint256 x) payable external{ OtherContract(otherContract).setX{value: msg.value}(x); } } ``` ### Call `call` 是`address`類型的低級成員函數,它用來與其他合約交互。它的返回值為`(bool, data)`,分別對應`call`是否成功以及目標函數的返回值。 - call 是 solidity 官方推薦的通過觸發 fallback 或 receive 函數發送 ETH 的方法。 - 不推薦用 `call` 來調用另一個合約,因為當你調用不安全合約的函數時,你就把主動權交給了它。推薦的方法仍是聲明合約變量後調用函數 - 當我們不知道對方合約的源代碼或 ABI ,就沒法生成合約變量;這時,我們仍可以通過`call`調用對方合約的函數。 #### call 的使用規則 `目標合约地址(address).call(二進制编碼)`; 其中二進制编碼利用結構化編碼函數 `abi.encodeWithSignature` 獲得 `abi.encodeWithSignature("函數簽名", 逗號分隔的具體參數)` 函數簽名為"函數名(逗號分隔的參數類型)"。例如`abi.encodeWithSignature("f(uint256,address)", _x, _addr)` 另外 call 在調用合約時可以指定交易發送的 ETH 數額和 gas: `目標合約地址.call{value:發送數額, gas:gas數額}(二進制編碼);` ```solidity! address(nameReg).call{gas: 1000000}(abi.encodeWithSignature("register(string)", "MyName")); address(nameReg).call{value: 1 ether}(abi.encodeWithSignature("register(string)", "MyName")); address.call{gas: 1000000, value: 1 ether} ``` ```solidity= // SPDX-License-Identifier: GPL-3.0 pragma solidity >=0.7.0 <0.9.0; contract OtherContract { uint256 private _x = 0; // 狀態變數_x // 收到eth的事件,記錄amount和gas event Log(uint amount, uint gas); receive() external payable { } fallback() external payable{ } // 返回合約ETH餘額 function getBalance() view public returns(uint) { 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; } } contract callContract{ // 定義Response事件,輸出call回傳的結果success和data event Response(bool success, bytes 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); //釋放事件 } function callGetX(address _addr) external returns(uint256){ // call getX() (bool success, bytes memory data) = _addr.call( abi.encodeWithSignature("getX()") ); emit Response(success, data); //釋放事件 return abi.decode(data, (uint256)); } function callNonExist(address _addr, uint256 x) external{ // call getX() (bool success, bytes memory data) = _addr.call( abi.encodeWithSignature("foo(uint256)", x) ); emit Response(success, data); //釋放事件 } } ``` ### Delegatecall delegate 中是委託 / 代表的意思, delegatecall 與 call 類似,是 solidity 中地址類型的低級成員函數。 #### 情境 當用戶 A 通過合約 B 來 call 合約 C 的時候,執行的是合約 C 的函數,語境(Context,可以理解為包含變量和狀態的環境)也是合約 C 的:msg.sender 是 B 的地址,並且如果函數改變一些狀態變量,產生的效果會作用於合約 C 的變量上。 ![](https://i.imgur.com/E7E8NBj.png) 而當用戶 A 通過合約 B 來 delegatecall 合約 C 的時候,執行的是合約 C 的函數,但是語境仍是合約 B 的: msg.sender 是 A 的地址,並且如果函數改變一些狀態變量,產生的效果會作用於合約 B 的變量上。 ![](https://i.imgur.com/AnjTvPb.png) 可以這樣理解:一個富商把它的資產(狀態變量)都交給一個 VC 代理(目標合約的函數)來打理。執行的是 VC 的函數,但是改變的是富商的狀態。 delegatecall 語法和 call 類似,不一樣的是 delegatecall 在調用合約時可以指定交易發送的 gas,但不能指定發送的 ETH 數額 #### 什麼情況下會用到 delegatecall? 目前 delegatecall 主要有兩個應用場景: - 代理合約( Proxy Contract ):將智能合約的存儲合約和邏輯合約分開: - 代理合約( Proxy Contract )存儲所有相關的變量,並且保存邏輯合約的地址、所有函數存在邏輯合約( Logic Contract )裡,通過 delegatecall 執行。 - 當升級時,只需要將代理合約指向新的邏輯合約即可。 - EIP - 2535 Diamonds(鑽石):鑽石是一個支持構建可在生產中擴展的模塊化智能合約系統的標準。鑽石是具有多個實施合同的代理合同 在使用 delegatecall 的時候要確保委託和被委託的合約參數類型一樣,順序也要一樣 ```solidity= // SPDX-License-Identifier: GPL-3.0 pragma solidity >=0.7.0 <0.9.0; contract B{ uint256 public number; string public name; function setName(string memory _name, address _addr) external { (bool success, ) = _addr.call(abi.encodeWithSignature("setName(string)", _name)); require(success); } function setNumber(uint256 _number, address _addr) external { (bool success, ) = _addr.delegatecall(abi.encodeWithSignature("setNumber(uint256)", _number)); require(success); } } contract C{ uint256 public number; string public name; function setName(string memory _name) external { name = _name; } function setNumber(uint256 _number) external { number = _number; } } ``` ![](https://hackmd.io/_uploads/rJeHnUGbT.png) ![](https://hackmd.io/_uploads/SJ-Ih8Mb6.png) ### 在合約中創建新合約 - Create & Create2 & Create3 在以太坊鏈上,用戶(外部帳戶,EOA)可以創建智能合約,智能合約同樣也可以創建新的智能合約。去中心化交易所 uniswap 就是利用工廠合約( Factory )創建了無數個幣對合約( Pair ) 有兩種方法可以在合約中創建新合約 1. create 2. create2 #### create ```solidity! Contract x = new Contract{value: _value}(params) ``` 其中 Contract 是要創建的合約名,x 是合約對象(地址),如果構造函數是 payable,可以創建時轉入 _value 數量的 ETH,params 是新合約構造函數的參數 #### CREATE2 ```solidity! Contract x = new Contract{salt: _salt, value: _value}(params) ``` 其中 Contract 是要創建的合約名,x 是合約對象(地址),`_salt`是指定的鹽;如果構造函數是 payable,可以創建時轉入`_value`數量的 ETH,params 是新合約構造函數的參數。 CREATE2 操作碼使我們在智能合約部署在以太坊網絡之前就能預測合約的地址。 Uniswap 創建 Pair 合約用的就是 CREATE2 而不是 CREATE #### CREATE 如何計算地址 智能合約可以由其他合約和普通賬戶利用 CREATE 操作碼創建。在這兩種情況下,新合約的地址都以相同的方式計算:創建者的地址( 通常為部署的錢包地址或者合約地址 )和 nonce ( 該地址發送交易的總數,對於合約賬戶是創建的合約總數,每創建一個合約 nonce+1 )的哈希。 ```solidity= 新地址 = hash(創建者地址, nonce) ``` 創建者地址不會變,但 nonce 可能會隨時間而改變,因此用 CREATE 創建的合約地址不好預測 #### CREATE2 如何計算地址​ CREATE2 的目的是為了讓合約地址獨立於未來的事件。不管未來區塊鏈上發生了什麼,你都可以把合約部署在事先計算好的地址上。用 CREATE2 創建的合約地址由 4 個部分決定: 1. 0xFF:一個常數,避免和 CREATE 衝突 2. 創建者地址 3. salt:一個創建者給定的數值 4. 待部署合約的字節碼( bytecode ) ```solidity= 新地址 = hash("0xFF", 創建者地址, salt, bytecode) ``` CREATE2 確保如果創建者使用 CREATE2 和提供的 salt 部署給定的合約 bytecode,它將存儲在新地址中 #### Create3 [CREATE3 多鏈部署合約於相同地址](https://medium.com/taipei-ethereum-meetup/create3-deploy-contract-multichain-c92de4241614) #### 極簡 Uniswap Uniswap V2 核心合約中包含兩個合約: - UniswapV2Pair : 幣對合約,用於管理幣對地址、流動性、買賣。 - UniswapV2Factory : 工廠合約,用於創建新的幣對,並管理幣對地址。 下面用 create 方法實現一個極簡版的 Uniswap:Pair 幣對合約負責管理幣對地址, PairFactory 工廠合約用於創建新的幣對,並管理幣對地址 #### Pair 合約 ```solidity= 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 更新為幣對中兩種代幣的地址 #### PairFactory ```solidity= 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 合約。其中 ```solidity= Pair pair = new Pair(); ``` 是創建合約的代碼。可以部署好 PairFactory 合約,然後用下面兩個地址作為參數調用 createPair ,看看創建的幣對地址是什麼: ```solidity= WBNB地址: 0x2c44b726ADF1963cA47Af88B284C06f30380fC78 BSC鏈上的PEOPLE地址: 0xbb4CdB9CBd36B01bD1cBaEBF2De08d9173bc095c ``` ### 刪除合約 - selfdestruct selfdestruct 命令可以用來刪除智能合約,並將該合約剩餘 ETH 轉到指定地址。 selfdestruct 是為了應對合約出錯的極端情況而設計的 ```solidity! selfdestruct(_addr); ``` :::info 注意事項 1. 對外提供合約銷毀接口時,最好設置為只有合約所有者可以調用,可以使用函數修飾符onlyOwner進行函數聲明。 2. 當合約被銷毀後與智能合約的交互也能成功,並且返回0。 3. 當合約中有 selfdestruct 功能時常常會帶來安全問題和信任問題,合約中的 Selfdestruct 功能會為攻擊者打開攻擊向量(例如使用 selfdestruct 向一個合約頻繁轉入 token 進行攻擊,這將大大節省了 GAS 的費用,雖然很少人這麼做),此外,此功能還會降低用戶對合約的信心。 ::: ### ABI ( Application Binary Interface ) 編碼解碼 ABI ( Application Binary Interface,應用二進制接口 )是與以太坊智能合約交互的標準 數據基於他們的類型編碼;並且由於編碼後不包含類型信息,解碼時需要註明它們的類型 Solidity 中,ABI 編碼有 4 個函數: - `abi.encode` - `abi.encodePacked` - `abi.encodeWithSignature` - `abi.encodeWithSelector` ABI 解碼有1個函數:`abi.decode`,用於解碼`abi.encode`的數據 #### `abi.encode` 將給定參數利用 ABI 規則編碼。 ABI 被設計出來跟智能合約交互,他將每個參數填充為 32 字節的數據,並拼接在一起。如果你要和合約交互,你要用的就是`abi.encode` ```solidity= 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 時。 ```solidity= 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)"`。當調用其他合約的時候可以使用。 A function selector is the first 4 bytes in the hash of the function's prototype. A function protot 當我們調用智能合約時,本質上是向目標合約發送了一段 calldata,在 remix 中發送一次交易後,可以在詳細信息中看見 input 即為此次交易的 calldataype is defined as the function's name and its argument types by order. It allows you, for example, to **call a function without knowing its exact return-value type** ```solidity= function encodeWithSignature() public view returns(bytes memory result) { result = abi.encodeWithSignature("foo(uint256,address,string,uint256[2])", x, addr, name, array); } ``` 編碼的結果為 ```! 0xe87082f1000000000000000000000000000000000000000000000000000000000000000a0000000000000000000000007a58c0be72be218b41c608b7fe7c5bb630736c7100000000000000000000000000000000000000000000000000000000000000a00000000000000000000000000000000000000000000000000000000000000005000000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000043078414100000000000000000000000000000000000000000000000000000000 ``` 等同於在`abi.encode`編碼結果前加上了 4 字節的函數選擇器說明。 說明: 函數選擇器就是通過函數名和參數進行簽名處理( Keccak–Sha3 )來標識函數,可以用於不同合約之間的函數調用 #### `abi.encodeWithSelector` 與`abi.encodeWithSignature`功能類似,只不過第一個參數為函數選擇器,為函數簽名 Keccak 哈希的前 4 個字節。 ```solidity= function encodeWithSelector() public view returns(bytes memory result) { result = abi.encodeWithSelector(bytes4(keccak256("foo(uint256,address,string,uint256[2])")), x, addr, name, array); } ``` 編碼的結果為 ```! 0xe87082f1000000000000000000000000000000000000000000000000000000000000000a0000000000000000000000007a58c0be72be218b41c608b7fe7c5bb630736c7100000000000000000000000000000000000000000000000000000000000000a00000000000000000000000000000000000000000000000000000000000000005000000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000043078414100000000000000000000000000000000000000000000000000000000 ``` 與`abi.encodeWithSignature`結果一樣。 #### `abi.decode` `abi.decode`用於解碼`abi.encode`生成的二進制編碼,將它還原成原本的參數。 ```solidity= function decode(bytes memory data) public pure returns(uint dx, address daddr, string memory dname, uint[2] memory darray) { (dx, daddr, dname, darray) = abi.decode(data, (uint, address, string, uint[2])); } ``` 我們將`abi.encode`的二進制編碼輸入給 decode,將解碼出原來的參數 #### ABI 的使用場景 1. 在合約開發中,ABI 常配合 call 來實現對合約的底層調用 2. `ethers.js`中常用 ABI 實現合約的導入和函數調用 3. 對不開源合約進行反編譯後,某些函數無法查到函數簽名,可通過 ABI 進行調用 ### Hash 哈希函數(hash function)是一個密碼學概念,它可以將任意長度的消息轉換為一個固定長度的值,這個值也稱作哈希(hash) #### Hash 的性質 一個好的哈希函數應該具有以下幾個特性: - 單向性:從輸入的消息到它的哈希的正向運算簡單且唯一確定,而反過來非常難,只能靠暴力枚舉。 - 靈敏性:輸入的消息改變一點對它的哈希改變很大。 - 高效性:從輸入的消息到哈希的運算高效。 - 均一性:每個哈希值被取到的概率應該基本相等。 - 抗碰撞性: - 弱抗碰撞性:給定一個消息 `x`,找到另一個消息 `x'` 使得 `hash(x)` = `hash(x')` 是困難的。 - 強抗碰撞性:找到任意 `x` 和 `x'`,使得 `hash(x)` = `hash(x')` 是困難的 #### Hash 的應用 - 生成數據唯一標識 - 加密簽名 - 安全加密 #### Keccak256 Keccak256 函數是 solidity 中最常用的哈希函數,用法非常簡單: ```solidity= 哈希 = keccak256(數據); ``` #### Keccak256 和 sha3 sha3 由 keccak 標準化而來,在很多場合下 Keccak 和 SHA3 是同義詞,但在 2015 年 8 月 SHA3 最終完成標準化時,NIST 調整了填充算法。 所以 **SHA3 就和 keccak 計算的結果不一樣**,這點在實際開發中要注意。 以太坊在開發的時候 sha3 還在標準化中,為了避免混淆,所以 Ethereum 和 Solidity 智能合約代碼中的 SHA3 是指 Keccak256 ,而不是標準的 NIST-SHA3,為了避免混淆,直接在合約代碼中寫成 Keccak256 是最清晰的 我們可以利用 keccak256 來生成一些數據的唯一標識 ### 函數選擇器 Selector 當我們調用智能合約時,本質上是向目標合約發送了一段 calldata,在 remix 中發送一次交易後,可以在詳細信息中看見 input 即為此次交易的 calldata,發送的 calldata 中前 4 個字節是 selector( 函數選擇器 ) calldata : ```solidity= 0x6a6278420000000000000000000000002c44b726adf1963ca47af88b284c06f30380fc78 ``` 這段很亂的字節碼可以分成兩部分 : ``` 前 4 個字節為函數選擇器 selector: 0x6a627842 後面 32 個字節為輸入的參數: 0x0000000000000000000000002c44b726adf1963ca47af88b284c06f30380fc78 ``` 其實 calldata 就是告訴智能合約,我要調用哪個函數,以及參數是什麼。 ### try-catch 在 solidity中,try-catch 只能被用於 external, public 函數或創建合約時 constructor (被視為 external 函數 )的調用 ```solidity= try externalContract.f() { // call 成功的情况下 運行一些代碼 } catch { // call 失败的情况下 運行一些代碼 } ``` 其中`externalContract.f()`是某個外部合約的函數調用,try 模塊在調用成功的情況下運行,而 catch 模塊則在調用失敗時運行。 同樣可以使用`this.f()`來替代`externalContract.f()`,`this.f()`也被視作為外部調用,但不可在構造函數中使用,因為此時合約還未創建。 如果調用的函數有返回值,那麼必須在 try 之後聲明 `returns(returnType val)`,並且在 try 模塊中可以使用返回的變量;如果是創建合約,那麼返回值是新創建的合約變量 ```solidity= try externalContract.f() returns(returnType val){ // call 成功的情况下 運行一些代碼 } catch { // call 失败的情况下 運行一些代碼 } ``` 另外,catch 模塊支持捕獲特殊的異常原因: ```solidity= try externalContract.f() returns(returnType){ // call 成功的情況下 運行一些代碼 } catch Error(string memory reason) { // 捕獲失敗的 revert() 和 require() } catch (bytes memory reason) { // 捕獲失敗的 assert() } ``` 只能用於外部合約調用和合約創建。 如果 try 執行成功,返回變量必須聲明,並且與返回的變量類型相同。 ```solidity= // SPDX-License-Identifier: GPL-3.0 pragma solidity >=0.7.0 <0.9.0; 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; } } contract TryCatch { // 成功 event event SuccessEvent(); // 失敗 event event CatchEvent(string message); event CatchByte(bytes data); // 宣告 OnlyEven 合約變數 OnlyEven even; constructor() { even = new OnlyEven(2); } // 在 external call 中使用 try-catch // execute(0)會成功並釋放`SuccessEvent` // execute(1)會失敗並釋放`CatchEvent` 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); } } // 在建立新合約中使用 try-catch (合約建立被視為 external call) // executeNew(0) 會失敗並釋放 `CatchEvent` // executeNew(1) 會失敗並釋放 `CatchByte` // executeNew(2) 會成功並釋放 `SuccessEvent` function executeNew(uint a) external returns (bool success) { try new OnlyEven(a) returns(OnlyEven _even){ // call 成功的情況下 emit SuccessEvent(); success = _even.onlyEven(a); } catch Error(string memory reason) { // catch revert("reasonString") 和 require(false, "reasonString") emit CatchEvent(reason); } catch (bytes memory reason) { // catch 失敗的 assert assert 失敗的錯誤型別是 Panic(uint256) 不是 Error(string) 型別 故會進入該分支 emit CatchByte(reason); } } } ```