# 課程 - 讀懂 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