# 課程 - 讀懂 Solidity 智能合約 ## 課程目標 * 了解智能合約的概念 * 讓 0 程式基礎的人能讀懂大部分 ERC20, ERC721 的相關 Solidity 合約 ## 備註 * 被我畫線劃掉的文字是正確但我認為不太重要的文字,可以先跳過不讀。 ## 智能合約的概念 智能合約是 * ~~Wikipedia 對智能合約的定義(最廣義)~~ * ~~[A smart contract is a computer program or a transaction protocol which is intended to automatically execute, control or document legally relevant events and actions according to the terms of a contract or an agreement.](https://en.wikipedia.org/wiki/Smart_contract)~~ * ethereum.org 對智能合約的定義 * [A "smart contract" is simply a program that runs on the Ethereum blockchain. It's a collection of code (its functions) and data (its state) that resides at a specific address on the Ethereum blockchain.](https://ethereum.org/en/developers/docs/smart-contracts/) * 自動販賣機 * 不去按它就不會動 ## Solidity 簡介 * 是目前最廣泛使用的 Ethereum 智能合約語言 * 但並不是唯一的 Ethereum 智能合約語言 * 因為被 TRON, BSC, Avalanche 抄去使用,所以在這些鏈上也可以使用 ## 看範例學 Solidity - SimpleStorage ```solidity= // SPDX-License-Identifier: MIT pragma solidity ^0.8.10; contract SimpleStorage { // State variable to store a number uint public num; // You need to send a transaction to write to a state variable. function set(uint _num) public { num = _num; } // You can read from a state variable without sending a transaction. function get() public view returns (uint) { return num; } } ``` source: https://solidity-by-example.org/state-variables/ deployed: https://rinkeby.etherscan.io/address/0xe42ac340e157a33ea51b685a7e61be647a9b4c7f#code 功能 * 在鏈上儲存一個數字並命名為 num * 可以透過 set 函式改寫這個數字 * 可以透過 get 函式讀取這個數字 ### version pragma ```solidity= pragma solidity ^0.8.10; ``` 以上這段程式碼告訴我們應該用 0.8.10 以上且低於 0.9.0 的 Solidity 編譯器編譯。 目前(2022/1/17)最新的 Solidity 版本是 0.8.11。 ### contract 合約 ```solidity= contract SimpleStorage { ... } ``` > Contracts in Solidity are similar to classes in object-oriented languages. They contain persistent data in state variables, and functions that can modify these variables. source: https://docs.soliditylang.org/en/latest/contracts.html#contracts Solidity 中「合約」的概念 * 合約將一段互相關聯的程式碼封裝起來,形成一個獨立的單位 * 合約是我們能部署(deploy)上鏈的基本單位 * 合約內通常會包含狀態變數(state variables)與可以讀寫狀態變數的函式(functions) 補充知識 * Solidity 合約會被轉換成 bytecode (位元組碼)後透過發一筆 transaction (交易)部署到鏈上,並存放在一個地址裡。被部署在鏈上的合約可以稱為一個 instance (實例)。 * 合約程式碼就像一份自動販賣機的設計圖 * 被部署上鏈後才真的做出會動的自動販賣機 * 我們可以部署同一份合約程式碼到不同地址,每個地址裡的合約實例會有各自的狀態,不互相影響。 ### comments 註解 ```solidity= // State variable to store a number uint public num; ``` 以上第一行為一個註解,目的是說明下一行的程式碼的作用。 關於註解 * 註解內的文字不會被執行 * 用途: * 解釋代碼的功用,供人閱讀 * 使部分程式碼暫時失去功能 語法一 ```solidity= // This is a comment. ``` 語法二 ```solidity= /* This is a comment. */ ``` 以下這個寫法是多行註解的習慣寫法,其實原理跟語法二相同 ```solidity= /** * @dev Returns the remaining number of tokens that `spender` will be * allowed to spend on behalf of `owner` through {transferFrom}. This is * zero by default. * * This value changes when {approve} or {transferFrom} are called. */ ``` ### variables 變數 ```solidity= uint public num; ``` 以上這段程式碼宣告了一個名為 num、型別為 uint、對外公開的變數。 在大部分程式設計中,變數是具有名稱的記憶體儲存空間。 在 Solidity 中,變數的儲存位置除了記憶體外,還有合約儲存空間(可以理解為存在區塊鏈上)。 #### 變數宣告方法 宣告一個名為 num、型別為 uint 的變數 ```solidity= uint num; ``` 可以在宣告時賦值 ```solidity= uint totalSupply = 1000; ``` #### 變數名稱不能重複 同一份合約內,變數名稱不能重複,所以 function set 裡面用 `_num` 來避免與 `num` 衝突 ```solidity= uint public num; function set(uint _num) public { num = _num; } ``` #### uint (type) * uint 是 unsigned integer 的簡寫,就是「不帶符號的整數」,符號指的就是負號,所以 uint 就是非負整數。 * uint 總共有 256 bits 的空間,可以儲存 0 ~ 2^256-1,大約是 1.1579 * 10^77 #### assign 賦值 Solidity 中的 `=` 符號是「賦值」的意思,也就是將等號右邊的值寫進等號左邊的變數裡。 以下寫法會使 num 的值變為 1000 ```solidity= num = 1000; ``` 以下寫法會使 num 的值變為 3 ```solidity= num = 1 + 2; ``` #### state variables, local variables 在 Solidity 中,變數根據儲存位置分為: * state variables 狀態變數 * 在 contract 內第一層宣告 * (可以理解為)會儲存在鏈上 * 這份合約中的 `num` 為 state variable * local variables 區域變數 * 在 function 內宣告,函式執行時暫時用到的變數 * 暫存在記憶體裡,不會儲存在鏈上 * 這份合約中的 `_num` 為 local variable #### public 在 Solidity 內,被宣告為 public 的變數可以被別的合約或帳戶讀取或呼叫。 與 public 相對的是 private,被宣告為 private 的變數無法直接被別的合約或帳戶讀取,寫法如下: ```solidity= uint private num; ``` ### functions 函式 ```solidity= function set(uint _num) public { num = _num; } // You can read from a state variable without sending a transaction. function get() public view returns (uint) { return num; } ``` 以上程式碼中宣告並定義了兩個函式:`set` 和 `get` `set` 被呼叫時必須傳入一個型別為 uint 的值,這個值會被暫時命名為 `_num`,然後寫入合約中的 `num` 變數中。 `get` 被呼叫時會讀取合約中的 `num` 變數的值,然後回傳這個值。 關於函式 * 函式通常包含一段具有特定功能的程式碼 * 函式被呼叫時才會執行函式內的程式碼 * 在這份程式碼中,並沒有「呼叫函式」的部分。這兩個函式只會被外部的合約或帳戶呼叫。 * 函式可以有輸入參數也可以沒有 * 函式可以有回傳值也可以沒有 #### public 在 Solidity 內,被宣告為 public 的函式可以被別的合約、帳戶呼叫,也可以在這份合約內被呼叫。 #### view 被設定為 view 這個函式只能讀取資料,而不能寫資料到鏈上。 #### returns ```solidity= returns (uint) ``` 用來說明這個函式會回傳什麼型別的資料 可以回傳不只一個值,例如以下 #### return ```solidity= return num; ``` 寫在函式內,用來回傳值給外部。 執行完 return 後,函式的執行就會結束。 --- ## 看範例學 Solidity - Ownable 上一個合約 SimpleStorage 的缺點是任何人都可以呼叫 `set` 函式,改變 `num` 的值。 我希望只有我能成功呼叫 `set` 函式改變 `num` 的值。 ```solidity= pragma solidity ^0.8.0; import "../utils/Context.sol"; abstract contract Ownable is Context { address private _owner; event OwnershipTransferred(address indexed previousOwner, address indexed newOwner); constructor() { _transferOwnership(_msgSender()); } function owner() public view virtual returns (address) { return _owner; } modifier onlyOwner() { require(owner() == _msgSender(), "Ownable: caller is not the owner"); _; } function renounceOwnership() public virtual onlyOwner { _transferOwnership(address(0)); } function transferOwnership(address newOwner) public virtual onlyOwner { require(newOwner != address(0), "Ownable: new owner is the zero address"); _transferOwnership(newOwner); } function _transferOwnership(address newOwner) internal virtual { address oldOwner = _owner; _owner = newOwner; emit OwnershipTransferred(oldOwner, newOwner); } } ``` 為了方便閱讀,我拿掉了註解。 source: https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/access/Ownable.sol ```solidity= pragma solidity ^0.8.0; abstract contract Context { function _msgSender() internal view virtual returns (address) { return msg.sender; } function _msgData() internal view virtual returns (bytes calldata) { return msg.data; } } ``` source: https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/utils/Context.sol ### import ```solidity= import "../utils/Context.sol"; ``` import 會把別的檔案的程式碼引入這份程式碼之中。 ### msg ```solidity= function _msgSender() internal view virtual returns (address) { return msg.sender; } function _msgData() internal view virtual returns (bytes calldata) { return msg.data; } ``` * msg 即 message,即在地址之間傳遞的資料。 * 例:我從我擁有私鑰的地址 A 發起一筆交易呼叫位於某個地址 B 的合約裡的某個 function F,同時傳入了一些參數和 1 ether。那麼這個過程中合約收到的所有資訊就稱為 message,包含: * msg.sender (address): sender of the message (current call) * 在這個例子中為我的地址 A * msg.value (uint): number of wei sent with the message * 在這個例子中為 10^18 * ~~msg.sig (bytes4): first four bytes of the calldata (i.e. function identifier)~~ * ~~在這個例子中為 function F 的 identifier~~ * ~~msg.data (bytes calldata): complete calldata~~ * ~~在這個例子中為 function F 的 identifier 和傳入的所有參數~~ * 最常用的是 msg.sender 和 msg.value ### 合約繼承 ```solidity= contract Ownable is Context ``` 會把 Context 的內容合併進 Ownable 這個合約,就能在 Ownable 合約中使用 Context 合約定義的函式和變數。 ### 加了底線的變數/函式名稱 ```solidity= function _transferOwnership() ``` ```solidity= address private _owner; ``` 是一個寫程式的習慣,代表這個變數或函式是不公開的或暫時的。 ### address (type) address 是一種資料型別,用來儲存以太坊上的地址,總共 160 bits。 ```solidity= address private _owner = 0xAb5801a7D398351b8bE11C439e05C5B3259aeC9B; ``` ### event event 的功用是記錄事件的發生,以方便之後搜尋這些事件。 例如 ERC721 token 有定義 Transfer 這個 event,每當有人轉移 token,合約就會發出一個 Transfer 記錄這次轉移的 from, to, tokenId,日後只要收集全部的 Transfer event 記錄就能知道每個 token 過去的流向。 ```solidity= event Transfer(address indexed from, address indexed to, uint256 indexed tokenId); ``` 定義 event ```solidity= event OwnershipTransferred(address indexed previousOwner, address indexed newOwner); ``` 發出 event ```solidity= emit OwnershipTransferred(oldOwner, newOwner); ``` ### constructor 建構子 ```solidity= constructor() { _transferOwnership(_msgSender()); } ``` constructor 是一種特殊的函式,會且只會在合約被創建時執行。可用來初始化一些變數。 在這個合約中,會在合約創建時,將 `_owner` 設為創建這個合約的地址。 ### require ```solidity= require(owner() == _msgSender(), "Ownable: caller is not the owner"); ``` require 有兩種語法: ```solidity= require(_Expression); ``` 如果 _Expression 為 true,就繼續往下執行。 如果 _Expression 為 false,就中斷執行,這筆交易就會失敗。 ---- ```solidity= require(_Expression, _ErrorMessage); ``` 如果 _Expression 為 true,就繼續往下執行。 如果 _Expression 為 false,就中斷執行,這筆交易就會失敗,並且會告訴我們交易失敗的原因為 _ErrorMessage。 ---- A == B: 確認 A 與 B 的值是否相同。 ---- ```solidity= require(owner() == _msgSender(), "Ownable: caller is not the owner"); ``` 所以這段程式的意思是: 如果這個 message 的發送者(多數情況下可以理解為交易的發送者)就是這個合約的 owner,就可以繼續往下執行。 如果這個 message 的發送者不是這個合約的 owner,交易就會立刻失敗,並且告訴我們失敗的原因是 "Ownable: caller is not the owner"。 ### modifier ```solidity= modifier onlyOwner() { require(owner() == _msgSender(), "Ownable: caller is not the owner"); _; } function renounceOwnership() public virtual onlyOwner { _transferOwnership(address(0)); } ``` modifier 的功能是把一段程式碼插到一個函式的程式碼的最頂端或最末端 `_;` 代表的是被插的函式本身的程式碼 所以以上的程式碼相當於 ```solidity= function renounceOwnership() public virtual { require(owner() == _msgSender(), "Ownable: caller is not the owner"); _transferOwnership(address(0)); } ``` ### internal ```solidity= function _transferOwnership(address newOwner) internal virtual { ... } ``` function 的 visibility (可見性)分為四種: * external: 合約內不能呼叫,只能從外部呼叫。 * public: 合約內可以呼叫,也能從外部呼叫,繼承了這個合約的合約也能呼叫。 * internal: 合約內可以呼叫,不能從外部呼叫,繼承了這個合約的合約也能呼叫。 * private: 只有這個合約可以呼叫,繼承了這個合約的合約不能呼叫。 詳見 https://docs.soliditylang.org/en/latest/contracts.html#visibility-and-getters 身為使用者只要知道: * external, public 我們可以呼叫 * internal, private 我們不可以呼叫 ### abstract contract, virtual function, override function ```solidity= abstract contract Ownable is Context { ... function renounceOwnership() public virtual onlyOwner { ... } function transferOwnership(address newOwner) public virtual onlyOwner { ... } ... } ``` (這段複雜且不太重要) 一個 function 是 virtual 代表這個 function 可以在被繼承後改寫(override)。 一個 contract 如果包含了 virtual function,則必須被宣告為 abstract。 一個 function 是 override 代表這個 function 改寫了某個繼承下來的 virtual function。 --- ## 看範例學 Solidity - OwnableSimpleStorage ```solidity= pragma solidity ^0.8.10; import "@openzeppelin/contracts/access/Ownable.sol"; contract OwnableSimpleStorage is Ownable { uint public num; function set(uint _num) public onlyOwner { num = _num; } function get() public view returns (uint) { return num; } } ``` deployed: https://rinkeby.etherscan.io/address/0xaBB0A1027cc3c5af7048010ECe6FFF21705890D4#code --- ## 看範例學 Solidity - IERC20 ```solidity= pragma solidity ^0.8.0; /** * @dev Interface of the ERC20 standard as defined in the EIP. */ interface IERC20 { function totalSupply() external view returns (uint256); function balanceOf(address account) external view returns (uint256); function transfer(address recipient, uint256 amount) external returns (bool); function allowance(address owner, address spender) external view returns (uint256); function approve(address spender, uint256 amount) external returns (bool); function transferFrom( address sender, address recipient, uint256 amount ) external returns (bool); event Transfer(address indexed from, address indexed to, uint256 value); event Approval(address indexed owner, address indexed spender, uint256 value); } ``` source: https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/token/ERC20/IERC20.sol EIP-20: https://eips.ethereum.org/EIPS/eip-20 ### interface 介面 * (可以理解為)interface 是一種特殊的合約,interface 之中不能實作函式,只能定義函式的傳入和回傳的格式。 * interface 最常見的用途是規定好 functions 和 events 的格式,然後讓別的合約繼承這個 interface,以確保合約的 functions 和 events 遵循特定格式。 * 目前最常見的例子是 IERC20, IERC721。 https://docs.soliditylang.org/en/latest/contracts.html#interfaces ### 了解 totalSupply, balanceOf, transfer * totalSupply: 查看 token 總量 * balanceOf: 查看某個地址擁有的 token 數量 * transfer: 轉移我的地址中的某個數量的 token 給另一個地址 ### 了解 approve, allowance, transferFrom * approve: 我允許某個地址轉移我擁有的 token。最常用於讓合約可以轉移我的 token。 * allowance: 查看某個地址(owner)已經允許某個地址(spender)轉移多少 token。 * transferFrom: 轉移別的地址中的某個數量的 token 給另一個地址。我必須已經被允許轉移大於等於這個數量的 token。最常用於讓合約轉移我的 token。 --- ## 看範例學 Solidity - ERC20 OpenZeppelin implementation: https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/token/ERC20/ERC20.sol ConsenSys implementation: https://github.com/ConsenSys/Tokens/blob/fdf687c69d998266a95f15216b1955a4965a0a6d/contracts/eip20/EIP20.sol ### types 型別 #### value types 值型別 | 型別 | 說明 | | -------- | -------- | | uint | 非負整數,共 256 bits。等價於 uint256。 | | uint8, uint16, uint24, ..., uint256 | 非負整數,分別具有 8, 16, 24, ..., 256 bits 的儲存空間。 | | int | 整數,可以是負數,共 256 bits。等價於 int256。 | | int8, int16, int24, ..., int256 | 整數,分別具有 8, 16, 24, ..., 256 bits 的儲存空間。 | | bool | 即 Boolean,布林值。存放 true 或 false。 | | address | 存放一個以太坊地址。 | | bytes1, bytes2, ..., bytes32 | 存放固定長度的任意資料。 | 程式碼範例: https://solidity-by-example.org/primitives/ #### 動態長度的 string and bytes | 型別 | 說明 | | -------- | -------- | | string | 存放任意長度的文字。 | | bytes | 存放任意長度的資料。 | ```solidity= string public name = "MyToken"; ``` ### mapping ```solidity= mapping(address => uint256) private _balances; ``` ```solidity= mapping(_KeyType => _ValueType) _VariableName; ``` mapping 是 Solidity 智能合約中非常常用的資料結構。 mapping 是一個一對一的對照表,你可以記錄和查詢某個 key 對應到什麼 value。 例如 `_balances` 記錄的是每個地址對應到的 token 數量。 #### mapping 的運作 以 `_balances` 為例 一開始 `_balances` 會是一個空的表。 這時候讀取任何地址對應到的值都會是 0。 ```solidity= mapping(address => uint256) private _balances; ``` | key | value | | -------- | -------- | | | | ```solidity= address addr1 = 0x1111111111111111111111111111111111111111; address addr2 = 0x2222222222222222222222222222222222222222; address addr3 = 0x3333333333333333333333333333333333333333; // read the value _balances[addr1]; // 0 // update the value _balances[addr1] = 1000; // 將 _balances[addr1] 裡面的數值設為 1000 _balances[addr2] = _balances[addr2] + 500; // 讓 _balances[addr2] 裡面的數值變成 _balances[addr2] 原本的數值加 500 _balances[addr3] += 300; // 讓 _balances[addr3] 裡面的數值增加 300 ``` | key | value | | -------- | -------- | | 0x1111111111111111111111111111111111111111 | 1000 | | 0x2222222222222222222222222222222222222222 | 500 | | 0x3333333333333333333333333333333333333333 | 300 | ```solidity= // transfer 100 from addr1 to addr2 _balances[addr1] -= 100; // 讓 _balances[addr1] 裡面的數值減少 100 _balances[addr2] += 100; // 讓 _balances[addr2] 裡面的數值增加 100 ``` | key | value | | -------- | -------- | | 0x1111111111111111111111111111111111111111 | 900 | | 0x2222222222222222222222222222222222222222 | 600 | | 0x3333333333333333333333333333333333333333 | 300 | 其他知識 * The _KeyType can be any built-in value type, bytes, string, or any contract or enum type. Other user-defined or complex types, such as mappings, structs or array types are not allowed. * _ValueType can be any type, including mappings, arrays and structs. ### decimals ether: 18 個小數點位數 1 ether =1.000000000 000000000 以太幣的最小單位: 10^-18 ether = 1 wei 10^18 wei ### transfer 失敗的話會發生什麼事情 * metamask 會提醒你交易會失敗 * 發到鏈上的話,交易會失敗,會消耗 gas ```solidity= if (allowance < MAX_UINT256) { allowed[_from][msg.sender] -= _value; } ``` // TODO --- ### mapping to a mapping ```solidity= mapping(address => mapping(address => uint256)) private _allowances; ``` ```solidity= mapping(address => ___________________________) private _allowances; mapping(address => uint256) ``` 第一層:地址對應到某個東西 第二層:這個「某個東西」是一個「地址對應到 uint256 的 mapping」 ![](https://i.imgur.com/qXA9LlR.png) `_allowances[addr1][addr2]` 代表「addr1 允許 addr2 轉移多少 token」 ### if-else #### if ```solidity= if (expression_1) { statement_1 } ... ``` 如果 expression_1 是 true,就執行 statement_1。 如果 expression_1 是 false,就不執行 statement_1。 無論如何,之後的 ... 都會繼續執行。 ---- #### if, else ```solidity= if (expression_1) { statement_1 } else { statement_2 } ... ``` 如果 expression_1 是 true,就執行 statement_1,然後往下執行 `...`。 如果 expression_1 是 false,就執行 statement_2,然後往下執行 `...`。 ---- #### if, else if, ..., else ```solidity= if (expression 1) { Statement(s) to be executed if expression 1 is true } else if (expression 2) { Statement(s) to be executed if expression 2 is true } else if (expression 3) { Statement(s) to be executed if expression 3 is true } else { Statement(s) to be executed if no expression is true } ``` ---- ### Comparisons: <=, <, ==, !=, >=, > * A <= B: 如果 A 小於等於 B,這個表達式就會是 true,否則為 false * A < B: 如果 A 小於 B,這個表達式就會是 true,否則為 false * A == B: 如果 A 等於 B,這個表達式就會是 true,否則為 false * A != B: 如果 A 不等於 B,這個表達式就會是 true,否則為 false * A >= B: 如果 A 大於等於 B,這個表達式就會是 true,否則為 false * A > B: 如果 A 大於 B,這個表達式就會是 true,否則為 false --- ## 看範例學 Solidity - WETH ```solidity= pragma solidity ^0.4.18; contract WETH9 { string public name = "Wrapped Ether"; string public symbol = "WETH"; uint8 public decimals = 18; event Approval(address indexed src, address indexed guy, uint wad); event Transfer(address indexed src, address indexed dst, uint wad); event Deposit(address indexed dst, uint wad); event Withdrawal(address indexed src, uint wad); mapping (address => uint) public balanceOf; mapping (address => mapping (address => uint)) public allowance; function() public payable { deposit(); } function deposit() public payable { balanceOf[msg.sender] += msg.value; Deposit(msg.sender, msg.value); } function withdraw(uint wad) public { require(balanceOf[msg.sender] >= wad); balanceOf[msg.sender] -= wad; msg.sender.transfer(wad); Withdrawal(msg.sender, wad); } function totalSupply() public view returns (uint) { return this.balance; } function approve(address guy, uint wad) public returns (bool) { allowance[msg.sender][guy] = wad; Approval(msg.sender, guy, wad); return true; } function transfer(address dst, uint wad) public returns (bool) { return transferFrom(msg.sender, dst, wad); } function transferFrom(address src, address dst, uint wad) public returns (bool) { require(balanceOf[src] >= wad); if (src != msg.sender && allowance[src][msg.sender] != uint(-1)) { require(allowance[src][msg.sender] >= wad); allowance[src][msg.sender] -= wad; } balanceOf[src] -= wad; balanceOf[dst] += wad; Transfer(src, dst, wad); return true; } } ``` WETH 是一個程式碼非常簡潔的 ERC20 代幣合約, 與一般的 ERC20 代幣合約的差別是: * WETH 的合約允許任何人存入 ETH 來換取 WETH token,透過 `deposit` 函式 * 也允許任何人將 WETH 換回 ETH 領出,透過 `withdraw` 函式。 * 若直接傳送 ETH 到 WETH 合約,會觸發 fallback function 然後呼叫 `deposit` 函式,其結果等同於存入 ETH 來換取 WETH token。 ### payable ```solidity= function deposit() public payable { balanceOf[msg.sender] += msg.value; Deposit(msg.sender, msg.value); } ``` 一個 function 是 payable 代表呼叫這個函式的同時可以傳入 ether。 沒有加 payable 的函式,不可以在呼叫時傳入 ether。 * msg.sender 代表呼叫這個函式的地址 * msg.value 代表呼叫這個函式時傳入的 ETH 數量,單位為 wei (10^(-18) ETH) 當這個 function 執行完成時,我的帳戶裡的 ETH 就會減少,合約裡的 ETH 就會增加。 並不需要在合約中寫減少呼叫者的以太幣數量。 ### fallback function ```solidity= function() public payable { deposit(); } ``` 沒有名字的 function() 是一種特殊的 function,稱為 fallback function,會在以下兩種情況被呼叫: * 從某個帳戶直接發送 ether 到這個合約,並且不指定呼叫的 function。 * 未來版本的 Solidity 讓這個情況會呼叫 receive() * 呼叫這個合約裡不存在的 function。 * 未來版本的 Solidity 讓這個情況會呼叫 fallback() 在這個合約中,觸發 fallback function 後會觸發 deposit,所以當我們直接傳 ether 到這個合約,就會相當於我們呼叫了 deposit,我們會獲得相應的 WETH token。 ### && ```solidity= if (src != msg.sender && allowance[src][msg.sender] != uint(-1)) { require(allowance[src][msg.sender] >= wad); allowance[src][msg.sender] -= wad; } ``` A && B: 邏輯上的「且」的意思。如果 A 和 B 都是 true,則這個表達式就會是 true,否則為 false。 ### about emit event 0.4.x 的 Solidity 還沒有 emit 關鍵字, 所以這個合約中要發出 event 時都沒有寫 `emit`。 ```solidity= Transfer(src, dst, wad); ``` 現在的寫法應為 ```solidity= emit Transfer(src, dst, wad); ``` ## 看範例學 Solidity - IERC721 ```solidity= pragma solidity ^0.8.0; import "../../utils/introspection/IERC165.sol"; 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; } ``` source: https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/token/ERC721/IERC721.sol ## 看範例學 Solidity - ERC721 source: https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/token/ERC721/ERC721.sol [EIP-721: Non-Fungible Token Standard](https://eips.ethereum.org/EIPS/eip-721) ### ERC721 合約內儲存的資料 ```solidity= mapping(uint256 => address) private _owners; ``` Mapping from token ID to owner address 記錄每個 token (即 NFT,以 token ID 編號)的擁有者是誰。 ---- ```solidity= mapping(address => uint256) private _balances; ``` Mapping owner address to token count 記錄每個地址擁有多少個這個合約的 NFT。 ---- ```solidity= mapping(uint256 => address) private _tokenApprovals; ``` Mapping from token ID to approved address * 記錄每個 NFT 被 approve 給哪一個地址。該地址就有權利轉移該 NFT。 * 每個 NFT 同一時間只能被 approve 給一個地址。 * 在 OpenZeppelin 的實作中,當一個 NFT 被 transfer 給別人時,該 NFT 的 approval 就會被 reset 變成 address(0)。 * [source](https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/token/ERC721/ERC721.sol#L341) * 現實中應該是很少用。setApprovalForAll 和 operator 較常用 ---- ```solidity= mapping(address => mapping(address => bool)) private _operatorApprovals; ``` bool: 存放 true/false uint: 存放非負整數 address mapping(address => bool) _operatorApprovals[addr1][addr2]: true/false Mapping from owner to operator approvals 記錄每個地址是否允許某個地址作為 operator。 一個地址可以指派多個地址成為他的 operator。 你指派的 operator 可以任意轉移你擁有的這個合約的 NFT。 ### safeTransferFrom When transfer is complete, this function checks if `_to` is a smart contract (code size > 0). If so, it calls `onERC721Received` on `_to` and throws if the return value is not `bytes4(keccak256("onERC721Received(address,address,uint256,bytes)"))`. ### using ... for ... 代表套用某個 library 到某個型別上 Address.sol: https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/utils/Address.sol ### || A || B: 邏輯上的「或」的意思。只要 A, B 其中任一個是 true(包含兩個都是 true 的情況),則這個表達式就會是 true,否則為 false。 ### ... ? ... : ... 這個東西稱為 ternary operator,中文是「三元運算子」 ```solidity= A ? B : C ``` 這個東西會根據 A 是 true 還是 false 而決定輸出什麼。 如果 A 是 true B 或 C ```solidity= function tokenURI(uint256 tokenId) public view virtual override returns (string memory) { require(_exists(tokenId), "ERC721Metadata: URI query for nonexistent token"); string memory baseURI = _baseURI(); return bytes(baseURI).length > 0 ? string(abi.encodePacked(baseURI, tokenId.toString())) : ""; } ``` 其中的 ```solidity= return bytes(baseURI).length > 0 ? string(abi.encodePacked(baseURI, tokenId.toString())) : ""; ``` 等價於 ```solidity= // 如果 baseURI 不是空的 if (bytes(baseURI).length > 0) { // 會將 baseURI 和 tokenId 串接起來,然後回傳 return string(abi.encodePacked(baseURI, tokenId.toString())); } else { return ""; } ``` 例如如果 baseURI 是 "ipfs://QmdyZc2HnXsTn3xKKPES4mBHxmk8ceLsMD8hd2N5GYcL5H/" 那當我們呼叫 tokenURI(0) 就會得到 ["ipfs://QmdyZc2HnXsTn3xKKPES4mBHxmk8ceLsMD8hd2N5GYcL5H/0"](ipfs://QmdyZc2HnXsTn3xKKPES4mBHxmk8ceLsMD8hd2N5GYcL5H/0) ### try ... catch ... ```solidity= try IERC721Receiver(to).onERC721Received(_msgSender(), from, tokenId, _data) returns (bytes4 retval) { // _Statement_1 } catch (bytes memory reason) { // _Statement_2 } ``` 呼叫 `try` 右邊的 function, 如果呼叫成功,就執行 _Statement_1; 如果呼叫失敗,就執行 _Statement_2。 ## 看範例學 Solidity - ERC1155 可以想像 ERC1155 為多個 ERC20 集合在一個合約裡。 也可以想像 ERC1155 為 ERC721,但每個編號的 token 可以不只一個。 EIP-1155: https://eips.ethereum.org/EIPS/eip-1155 IERC1155: https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/token/ERC1155/IERC1155.sol ERC1155: https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/token/ERC1155/ERC1155.sol ### IERC1155 ```solidity= interface IERC1155 is IERC165 { event TransferSingle(address indexed operator, address indexed from, address indexed to, uint256 id, uint256 value); event TransferBatch( address indexed operator, address indexed from, address indexed to, uint256[] ids, uint256[] values ); event ApprovalForAll(address indexed account, address indexed operator, bool approved); event URI(string value, uint256 indexed id); function balanceOf(address account, uint256 id) external view returns (uint256); function balanceOfBatch(address[] calldata accounts, uint256[] calldata ids) external view returns (uint256[] memory); function setApprovalForAll(address operator, bool approved) external; function isApprovedForAll(address account, address operator) external view returns (bool); function safeTransferFrom( address from, address to, uint256 id, uint256 amount, bytes calldata data ) external; function safeBatchTransferFrom( address from, address to, uint256[] calldata ids, uint256[] calldata amounts, bytes calldata data ) external; } ``` ### ERC1155 合約內儲存的資料 ```solidity= mapping(uint256 => mapping(address => uint256)) private _balances; ``` Mapping from token ID to account balances _balances[tokenId][user]: balance ---- ```solidity= mapping(address => mapping(address => bool)) private _operatorApprovals; ``` Mapping from account to operator approvals ## 看範例學 Solidity - FomoDog source: https://etherscan.io/address/0x90cfce78f5ed32f9490fd265d16c77a8b5320bd4#code ## 參考資料、學習資源 * [Solidity documentation](https://docs.soliditylang.org/en/latest/index.html) * [OpenZeppelin | Contracts](https://openzeppelin.com/contracts/) * [Solidity by Example](https://solidity-by-example.org/) hash("abc")=> 100377377485549772206696113518786163793871789198621204599088125860236924813313 --- ## Q&A ### 關於 ERC20 approve 的安全性 ERC20 approve 的作用是讓某個地址可以轉移你擁有的某個數量的 ERC20 token。 通常如果是 approve 一個智能合約動用你的 token,都會 approve 最大數量,也就是 2^256 - 1,就相當於讓那個智能合約任意動用你的 token。 例如在 Uniswap, Compound, Opensea 都有這樣的設計。 單一筆交易只能 approve 一種 token。例如目前無法做到只發一筆交易就同時 approve WETH 和 USDC。 為什麼應該在不用某個 dapp 了之後就 unapprove (approve 0)? * 這是好習慣,可以避免後續風險 * 不 unapprove 的話,如果合約有瑕疵,或合約開發者在合約裡埋了後門(在 contract 內有幾個 function 有很大的彈性(例如從合約發出任何內容的交易)可以做到很多事情),就有可能盜走你 approve 過的 token ### 一個 sign 可以做到多糟糕的事情 我們在 sign transaction (交易)的時候, 其實會先把 transaction 的內容經過 hash 變成一個 256 bit 的看似亂碼的資料, 然後才用你的私鑰簽名。 所以當你簽一個來路不明的看似亂碼的 256 bit 的東西, 例如 0xc43f3b51c547bd7e4a852393953f11d2ae1b209a8d9d3e50a8664a2cdfa084c3 最糟的情況是你簽了一個交易, 然後網站擁有者取得這個簽名後把交易發上鏈, 從你的錢包轉走資產。 單一筆交易能發生的最糟糕的幾件事: * 轉走你所有的 ETH * 轉走你所有的某一種代幣 * 允許別人轉走你某的一個 collection 的 NFT (setApprovalForAll),這跟「轉走你所有的某系列的 NFT」也差不多了。 以上三者不會同時發生 ### sign 的實作 善意的簽: signTransaction({ destination: }) 惡意的簽: sign(hash) ### metamask connect 的作用只是讓網站可以讀到你的地址,無法盜走你的資產,還蠻安全的。 ### metamask 生成的地址之間是什麼關係? * 這些地址的私鑰都是從同一組助記詞生成出來的,所以如果助記詞被盜,這些地址就都會被盜。 * 但對於區塊鏈、智能合約來說,這些地址彼此之間是完全獨立的。別人也無法看出兩個地址是否是來自同一組助記詞。 ### markdown ### 資料分析語言 python javascript 適合爬鏈上、合約資料 web3.js ### 什麼是 ABI? Application Binary Interface 一個 ABI 是一個合約與外界溝通的所有介面的資料,通常就是所有 function 的輸入和輸出的資訊。 只要有合約地址和 ABI,我們就可以與合約互動。 --- // TODO 1. CyberKongz banana 2. Scholarz 3. Superfarm https://github.com/chiru-labs/ERC721A/blob/main/contracts/ERC721A.sol 4. neo tokyo: 4 NFTS 組成一個 NFT bytes token vitalik 提出的 soulbound 數據分析 dune.xyz