⛓ 精通以太坊

Mastering Ethereum (2018)

去中心化的電腦

⚙️ 執行環境

🎯 什麼情境該想到我

當你要搞懂「以太坊上一筆交易到底送出了哪些欄位、為什麼會卡住、合約呼叫是怎麼被編碼進去的」時。

⚙️ 怎麼用(理解模型)

1. 只有兩種帳戶,而且只有一種能發起交易

以太坊沒有 UTXO,只有帳戶,且分兩類:

“Externally owned accounts are those that have a private key; having the private key means control over access to funds or contracts. Now, you’re probably guessing there is another type of account. That other type of account is a contract account. A contract account has smart contract code, which a simple EOA can’t have. Furthermore, a contract account does not have a private key. Instead, it is owned (and controlled) by the logic of its smart contract code: the software program recorded on the Ethereum blockchain at the contract account’s creation and executed by the EVM.”
(譯文:外部擁有帳戶就是那些擁有私鑰的帳戶;持有私鑰即代表能控制對資金或合約的存取。你大概已經猜到還有另一種帳戶,那就是合約帳戶。合約帳戶擁有智能合約程式碼,這是單純的 EOA 不會有的。此外,合約帳戶沒有私鑰,而是由它自己的智能合約程式碼邏輯所擁有與控制——那份程式在合約帳戶建立時就記錄在以太坊區塊鏈上,並由 EVM 執行。)
—— Ch.2 “Externally Owned Accounts (EOAs) and Contracts”(PDF p.90)

關鍵不對稱:

“Note that because a contract account does not have a private key, it cannot initiate a transaction. Only EOAs can initiate transactions, but contracts can react to transactions by calling other contracts, building complex execution paths.”
(譯文:注意,因為合約帳戶沒有私鑰,它無法發起交易。只有 EOA 能發起交易,但合約可以對交易做出反應、去呼叫其他合約,組成複雜的執行路徑。)
—— Ch.2 “Externally Owned Accounts (EOAs) and Contracts”(PDF p.90)

所以「一切都從一筆 EOA 簽的交易開始」——書在 Ch.6 開頭就把話講死:

“Transactions are signed messages originated by an externally owned account, transmitted by the Ethereum network, and recorded on the Ethereum blockchain. … Contracts don’t run on their own. Ethereum doesn’t run autonomously. Everything starts with a transaction.”
(譯文:交易是由外部擁有帳戶發起、經以太坊網路傳播、並記錄在以太坊區塊鏈上的已簽章訊息。……合約不會自己跑。以太坊不會自動運轉。一切都從一筆交易開始。)
—— Ch.6 開篇(PDF p.207)

2. 一筆交易到底送出了什麼(序列化欄位)

網路上實際傳的是 RLP 編碼的二進位訊息,只含這些欄位(書中逐欄定義,此處為譯文整理):

欄位書中定義(譯文)
nonce由發起的 EOA 給出的序號,用來防止訊息重放
gas price發起人願意支付的 gas 價格(以 wei 計)
gas limit發起人願意為這筆交易購買的 gas 上限
recipient目的地以太坊地址
value要送到目的地的 ether 數量
data變動長度的二進位資料負載
v, r, s發起 EOA 的 ECDSA 數位簽章的三個組成

“Note that the field labels (to, gas limit, etc.) are shown here for clarity, but are not part of the transaction serialized data, which contains the field values RLP-encoded.”
(譯文:請注意,這裡列出的欄位名稱(to、gas limit 等)只是為了說明清楚,它們並不是交易序列化資料的一部分;序列化資料裡只有 RLP 編碼後的欄位值。)
—— Ch.6 “The Structure of a Transaction”(PDF p.209)

沒有 from 欄位——這是最反直覺的一點:

“For example, you may notice there is no “from” data in the address identifying the originator EOA. That is because the EOA’s public key can be derived from the v,r,s components of the ECDSA signature. The address can, in turn, be derived from the public key.”
(譯文:舉例來說,你可能會注意到裡面沒有標示發起者 EOA 的「from」資料。那是因為 EOA 的公鑰可以從 ECDSA 簽章的 v、r、s 三個組成推導出來,而地址又可以進一步從公鑰推導出來。)
—— Ch.6 “The Structure of a Transaction”(PDF p.209)

你在區塊瀏覽器看到的 from、區塊高度、交易 ID,都是軟體事後從交易「推導」出來加上去的,不在訊息本體裡。

3. nonce:既排順序,也防重放

書給的兩個場景(Ch.6 “The Transaction Nonce”, PDF p.210–211):

  • 排序:你先送 6 ether、再送 8 ether,但帳上只有 10 ether。去中心化網路裡節點收到的先後是隨機的;有了 nonce(例如 3 和 4),nonce 4 那筆會被擱著,直到 0–3 都處理完為止。
  • 防重放:沒有 nonce 的話,兩筆「同金額同收款人」的交易長得一模一樣,任何看到你交易的人都能複製貼上重送到你的 ether 被榨乾。有 nonce,每一筆都獨一無二。

書明講這正是帳戶模型必須付出的代價:

“In summary, it is important to note that the use of the nonce is actually vital for an account-based protocol, in contrast to the “Unspent Transaction Output” (UTXO) mechanism of the Bitcoin protocol.”
(譯文:總結來說,必須注意的是:對一個以帳戶為基礎的協定而言,nonce 的使用是至關重要的;這與比特幣協定的「未花費交易輸出」(UTXO)機制形成對比。)
—— Ch.6 “The Transaction Nonce”(PDF p.211)

實務上 nonce 從 0 開始計數,且不是明著存在帳戶狀態裡,而是「數這個地址已確認的交易筆數」動態算出來的。查法:web3.eth.getTransactionCount(address)

4. 交易卡住=nonce 有缺口

“The Ethereum network processes transactions sequentially, based on the nonce. That means that if you transmit a transaction with nonce 0 and then transmit a transaction with nonce 2, the second transaction will not be included in any blocks. It will be stored in the mempool, while the Ethereum network waits for the missing nonce to appear.”
(譯文:以太坊網路依照 nonce 依序處理交易。這表示如果你送出一筆 nonce 為 0 的交易,接著送出一筆 nonce 為 2 的交易,第二筆不會被納入任何區塊。它會被存在 mempool 裡,網路則等著缺掉的那個 nonce 出現。)
—— Ch.6 “Gaps in Nonces, Duplicate Nonces, and Confirmation”(PDF p.215)

推論出三條處置原則(全部照書):

  1. 補洞就通:把缺的那個 nonce 用一筆有效交易送出去,後面卡住的會一起被打包。
  2. 補洞不可逆:一旦缺號那筆被驗證,所有後續已廣播的交易會陸續變有效——it is not possible to “recall” a transaction!(譯文:你沒辦法「收回」一筆交易!)
  3. 重複 nonce 是賭運氣:同 nonce 送兩筆不同內容,一筆確認一筆被拒,哪筆贏取決於誰先到達第一個驗證節點,基本上是隨機的。

5. value 與 data 的四種組合

“A transaction with only value is a payment. A transaction with only data is an invocation. A transaction with both value and data is both a payment and an invocation. A transaction with neither value nor data — well that’s probably just a waste of gas! But it is still possible.”
(譯文:只有 value 的交易是一筆付款。只有 data 的交易是一次呼叫。同時有 value 和 data 的交易既是付款也是呼叫。兩者都沒有的交易——嗯,那大概只是在浪費 gas!但它仍然是可能的。)
—— Ch.6 “Transaction Value and Data”(PDF p.223)

收款方是 EOA 還是合約,行為不同:送到 EOA 只是加餘額,data 會被協定忽略(怎麼解讀由錢包自己決定,且不受共識規則約束);送到合約則會讓 EVM 執行合約,並嘗試呼叫 data 裡指名的 function。沒有 data 時會呼叫 fallback function,若該 function 是 payable 就執行它;連 fallback 都沒有,效果就只是增加合約餘額。

6. 合約呼叫怎麼被編碼成 data

“The data payload sent to an ABI-compatible contract (which you can assume all contracts are) is a hex-serialized encoding of: A function selector — The first 4 bytes of the Keccak-256 hash of the function’s prototype. This allows the contract to unambiguously identify which function you wish to invoke. The function arguments — The function’s arguments, encoded according to the rules for the various elementary types defined in the ABI specification.”
(譯文:送往 ABI 相容合約(你可以假設所有合約都是)的資料負載,是以下兩者的十六進位序列化編碼:函式選擇器——函式原型的 Keccak-256 雜湊值的前 4 個位元組,讓合約能毫不含糊地辨識出你想呼叫哪個函式。函式引數——依照 ABI 規格中各種基本型別的規則編碼的函式引數。)
—— Ch.6 “Transmitting a Data Payload to an EOA or Contract”(PDF p.227)

書上的完整範例(Ch.6, PDF p.227–228):

  1. 函式原型 = 函式名 + 括號內以逗號分隔的各引數型別 → withdraw(uint256)uintuint256 的別名)。
  2. web3.sha3("withdraw(uint256)")0x2e1a7d4d1332…,取前 4 bytes → 0x2e1a7d4d 就是 selector。
  3. 引數 0.01 ether = 10000000000000000 wei = 0x2386f26fc10000,補齊到 32 bytes。
  4. 串起來就是 data:
    2e1a7d4d000000000000000000000000000000000000000000000000002386f26fc10000

7. 簽章:簽的是雜湊,不是交易本身

“When we say “sign the transaction” we actually mean “sign the Keccak-256 hash of the RLP-serialized transaction data.” The signature is applied to the hash of the transaction data, not the transaction itself.”
(譯文:當我們說「簽署交易」時,實際的意思是「簽署 RLP 序列化交易資料的 Keccak-256 雜湊值」。簽章是施加在交易資料的雜湊上,而不是交易本身。)
—— Ch.6 “Transaction Signing in Practice”(PDF p.240)

五個步驟(照書):

  1. 建立含九個欄位的交易資料結構:nonce、gasPrice、gasLimit、to、value、data、chainID、0、0。
  2. 對這個結構做 RLP 序列化。
  3. 算它的 Keccak-256 雜湊。
  4. 用發起 EOA 的私鑰對雜湊算 ECDSA 簽章。
  5. 把算出的 v、r、s 附加回交易。

多出來的 chainID、0、0 來自 EIP-155「Simple Replay Attack Protection」(Spurious Dragon 硬分叉,區塊 #2,675,000 導入):把鏈識別碼放進被簽的資料裡,一改鏈 ID 簽章就失效,因此為 A 鏈造的交易無法在 B 鏈重放。主網 chain ID = 1、Ropsten = 3、Rinkeby = 4、Kovan = 42、Ethereum Classic 主網 = 61。

v 同時編碼兩件事:chain ID,以及復原識別碼(27/28 是舊式簽章,35/36 是完整 Spurious Dragon 式),後者用來標示公鑰 y 座標的奇偶。

8. 公鑰復原(recovery):from 是算出來的

從 r、s 可以算出兩個可能的公鑰(因為橢圓曲線對 x 軸對稱,同一個 x 有兩個點 R 與 R’):K₁ = r⁻¹(sR − zG)、K₂ = r⁻¹(sR’ − zG),其中 z 是訊息雜湊的最低 n 位元、G 是曲線生成點。

“To make things more efficient, the transaction signature includes a prefix value v, which tells us which of the two possible R values is the ephemeral public key. If v is even, then R is the correct value. If v is odd, then it is R’. That way, we need to calculate only one value for R and only one value for K.”
(譯文:為了讓過程更有效率,交易簽章包含一個前綴值 v,告訴我們兩個可能的 R 值中哪一個才是臨時公鑰。如果 v 是偶數,R 就是正確的值;如果 v 是奇數,那就是 R’。這樣一來我們只需要計算一個 R 值和一個 K 值。)
—— Ch.6 “The Signature Prefix Value (v) and Public Key Recovery”(PDF p.246)

9. 建立合約=送到零地址

合約建立交易的 to 欄位填 0x0,data 放編譯好的 bytecode。零地址既不是 EOA 也不是合約,永遠不能花錢也不能發起交易,只作為目的地、代表「建立這份合約」。想燒幣請用專用的 burn address 0x000000000000000000000000000000000000dEaD,別用 0x0。交易收據(receipt)裡的 contractAddress 就是新合約地址。

🧪 我實際套用的紀錄

  • (待填)

⚠️ 注意 / 什麼時候不適用

  • 本卡的 gas 機制是本書(2018)當時的單一 gasPrice 模型gasPrice 是發起人自己設定的、以 wei/gas 計價的單一價格,出價越高越快確認,最低可以設 0(協定不禁止免費交易,只是可能永遠不被確認)。書用 MetaMask 的例子是 3 gwei × 21,000 gas = 0.000063 ETH。書裡就是這樣寫的,本卡不做任何現代化改寫(見下方「刻意不寫」)。
  • gasLimit 是「願意買的上限」,不是預付:驗證時只檢查餘額夠不夠付 gasPrice * gas,實際只按用掉的量計費;但送出前餘額必須足以支付你願付的最大值。收款方若是合約,gas 用量只能估、無法精確預測(不同執行路徑成本不同)。
  • 協定不驗證收款地址:任何 20 bytes 值都算合法。送到沒有對應私鑰也沒有合約的地址=把 ether 燒掉、永遠拿不回來。驗證只能在 UI 層做。
  • 併發送交易很危險:多台機器用同一個地址簽交易時 nonce 極難協調;指派 nonce 的那台機器變成單點故障,而任何一個被指派卻沒用掉的 nonce 都會讓後續交易全部卡住。書的結論是:多數實作乾脆放棄併發,改用單一流程處理提領,或設多個各自獨立的熱錢包。
  • 別用 getTransactionCount 數 pending:短時間連送多筆時它數不準。只有在 pending 與 confirmed 數量相等時才可信;之後要靠應用自己追。Parity 的 parity_nextNonce 則能正確計數。
  • 以太坊沒有原生多簽:EOA 的基本轉帳「沒有任何多重簽章的機制」,多簽得靠錢包合約自己實作——彈性換來的是雙面刃,安全性完全取決於那份多簽合約的程式碼品質。
  • 這張卡只講到「交易被記錄上鏈」為止:EVM 內部怎麼執行、gas 每個 opcode 怎麼收,看 工具-Gas與EVM

🔗 相關工具

  • 工具-UTXO交易模型 —— 對照組:兩種記帳哲學。比特幣的帳本不存餘額、只存「還沒被花掉的輸出」,交易是「消耗舊 UTXO、產生新 UTXO」,防雙花靠「一個 UTXO 只能花一次」這件事本身;以太坊反過來,帳本直接存帳戶餘額與狀態,同一個帳戶會被反覆改寫,所以必須額外發明 nonce 來排序與防重放(本書 Ch.6 就是這樣點名對比的)。副作用也隨之而來:UTXO 天然可平行驗證、天然無序無關,帳戶模型則是嚴格序列、一個缺口就整串卡住。
  • 工具-Gas與EVM —— 同書:本卡的 gasPricegasLimit 兩個欄位只是入口,收費規則與 EVM 執行細節在那張。
  • 工具-金鑰地址與錢包 —— 金鑰那一層兩鏈共通:本卡的 v/r/s 簽章、以及「地址由公鑰推導」都建立在同一套橢圓曲線私鑰/公鑰上。
  • 精通以太坊 —— 來源書卡

🎯 什麼情境該想到我

當你想搞懂「以太坊上的成本到底從哪來」時——為什麼每個運算都要收費、為什麼寫 storage 特別貴、為什麼交易失敗了錢還是不見、為什麼一個無窮迴圈不會把整條鏈弄死。

本卡完全依據《Mastering Ethereum》(O’Reilly, 2018) Ch.13「The Ethereum Virtual Machine」。

⚙️ 怎麼用(理解機制)

1. EVM 是什麼樣的機器

先把它定位成「一台只會算數的虛擬機」,不是虛擬化整台電腦:

“The EVM operates in a much more limited domain: it is just a computation engine, and as such provides an abstraction of just computation and storage, similar to the Java Virtual Machine (JVM) specification, for example.”
(譯文:EVM 運作的領域侷限得多:它就只是一個運算引擎,因此提供的抽象也只有運算與儲存,類似於 Java 虛擬機(JVM)規格那樣。)
—— Ch.13 “Comparison with Existing Technology”(PDF p.546)

由此推出三個「它沒有的東西」(同小節):

  • 沒有排程能力——執行順序由外部決定,以太坊客戶端跑過已驗證的區塊交易,決定哪些合約要執行、以什麼順序執行。
  • 以太坊世界電腦是單執行緒的,就像 JavaScript。
  • 沒有「系統介面」處理、也沒有「硬體支援」——根本沒有實體機器可介接,這台世界電腦完全是虛擬的。

再來是它的架構。這句是整章的骨幹:

“The EVM has a stack-based architecture, storing all in-memory values on a stack. It works with a word size of 256 bits (mainly to facilitate native hashing and elliptic curve operations) and has several addressable data components:”
(譯文:EVM 採用堆疊式(stack-based)架構,把所有記憶體內的值都存放在一個堆疊上。它的字組大小(word size)是 256 位元(主要是為了方便原生的雜湊與橢圓曲線運算),並且有數個可定址的資料元件:)
—— Ch.13 “What Is the EVM?”(PDF p.543)

書列的三個可定址元件:

元件書中定義(譯文)
program code ROM不可變的程式碼 ROM,載入待執行的智能合約 bytecode
memory揮發性記憶體,每個位置都明確初始化為零
storage永久儲存,是以太坊狀態的一部分,同樣初始化為零

外加「一組在執行期間可取得的環境變數與資料」。

2. 三種空間:stack / memory / storage

這是寫合約時最該內化的區分。書的資訊分散在架構說明、opcode 分類與 gas 三處,整理如下:

生命週期存取用的 opcode書中怎麼說
stack(堆疊)單次執行內PUSHx(x = 1–32 bytes)、POPDUPx(x = 1–16)、SWAPx(x = 1–16)所有 in-memory 值都放在 stack 上;一個 word 是 256 bits
memory(記憶體)揮發性,每次 EVM 實例化時「設為全零」MLOADMSTORE(存一個 word)、MSTORE8(存一個 byte)、MSIZE「使用 EVM memory 是有 gas 成本的」
storage(儲存)永久,屬於以太坊狀態的一部分SLOADSSTORE是「只有智能合約會用到的永久資料倉」;「把資料存進合約的鏈上 storage 也有 gas 成本」

運算怎麼吃到這些值:

“As you might expect, all operands are taken from the stack, and the result (where applicable) is often put back on the top of the stack.”
(譯文:如你所料,所有運算元都取自堆疊,而結果(若適用)通常會被放回堆疊頂端。)
—— Ch.13 “The EVM Instruction Set (Bytecode Operations)“(PDF p.547)

書用 MSTORE 走了一遍:它需要兩個引數,跟大多數 EVM 操作一樣從 stack 取得;每取一個引數就 pop 一次(頂端的值被拿走、其餘全部往上移一格)。第一個引數是 memory 位址、第二個是要存的值。

成本的相對關係,書只給了這幾句與這幾個數字(“Gas Accounting Considerations”,p.569):

  • 「運算越密集的操作,gas 越貴」——SHA3(30 gas)比 ADD(3 gas)貴 10 倍。
  • 有些操作(例如 EXP)還要「依運算元大小額外付費」。
  • 使用 memory、以及把資料寫進合約鏈上 storage,都各自有 gas 成本。

反過來,清掉 storage 是會退錢的(見下方第 6 節「負的 gas 成本」),從退款金額就看得出書把 storage 佔用當成很貴的資源。

⚠️ 書的正文沒有列出 SSTORE、memory 展開等的具體 gas 數字,只說完整的 opcode 與對應 gas 成本表在 Appendix C(Table C-1)。本卡不代書填這些數字。

3. 一次執行的生命週期:sandbox 與 world state

先看狀態長什麼樣(“Ethereum State”,p.551):world state 是「以太坊地址(160-bit 值)到帳戶」的對映;每個帳戶包含 ether 餘額(以 wei 計)、nonce、帳戶的 storage(永久資料倉,只有智能合約會用)、以及帳戶的程式碼(同樣只有合約帳戶才有)。EOA 永遠沒有程式碼,storage 也是空的。

當一筆交易導致合約程式碼執行,EVM 會被實例化(同小節):

  1. program code ROM 載入被呼叫的合約帳戶程式碼
  2. program counter 設為 0
  3. storage 從合約帳戶的 storage 載入
  4. memory 設為全零
  5. 所有區塊與環境變數設好
  6. 關鍵變數:gas supply,設成交易開始時發送者已付費購買的 gas 數量

接著:

“As code execution progresses, the gas supply is reduced according to the gas cost of the operations executed. If at any point the gas supply is reduced to zero we get an “Out of Gas” (OOG) exception; execution immediately halts and the transaction is abandoned. No changes to the Ethereum state are applied, except for the sender’s nonce being incremented and their ether balance going down to pay the block’s beneficiary for the resources used to execute the code to the halting point.”
(譯文:隨著程式碼執行推進,gas 供給量會依照所執行操作的 gas 成本而減少。如果在任何時點 gas 供給量降到零,我們就會得到一個「Out of Gas」(OOG)例外;執行立即中止,該筆交易被放棄。以太坊狀態不會套用任何變更,唯一的例外是發送者的 nonce 會遞增、且其 ether 餘額會減少,用以支付區塊受益者為了執行程式碼到中止點所耗用的資源。)
—— Ch.13 “Ethereum State”(PDF p.551–552)

書給的心智模型是沙盒:EVM 跑在以太坊 world state 的一份沙盒副本上,只要執行因任何理由無法完成,這份沙盒版本就被整份丟棄;若順利完成,真實狀態才更新成沙盒版本(包含被呼叫合約的 storage 變更、新建立的合約、以及所有已發起的 ether 餘額轉移)。

執行是遞迴的:合約可以呼叫其他合約,每次呼叫都會在新目標上再實例化一個 EVM,其沙盒狀態由上一層的沙盒初始化,並被指定一份 gas supply(當然不能超過上一層剩下的量),因此子層自己也可能因為分到的 gas 太少而以例外中止;這種情況下沙盒狀態被丟棄,執行回到上一層的 EVM。

4. opcode 的完整分類

書先給了一個概觀:EVM 指令集提供「算術與位元邏輯運算、執行脈絡查詢、stack/memory/storage 存取、控制流程操作、記錄(logging)/呼叫與其他運算子」,另外還能取得帳戶資訊(地址、餘額)與區塊資訊(區塊編號、當前 gas price)。

接著是那句分類宣告——“The available opcodes can be divided into the following categories:“(譯文:可用的 opcode 可以分成下列幾類:),完整 7 類如下(“The EVM Instruction Set (Bytecode Operations)“,p.547–550):

分類書中說明代表 opcode
Arithmetic operations(算術)所有算術都是模 2²⁵⁶ 運算(除非另有說明),且 0⁰ 視為 1ADD MUL SUB DIV SDIV MOD SMOD ADDMOD MULMOD EXP SIGNEXTEND SHA3
Stack operations(堆疊)stack、memory、storage 的管理指令POP MLOAD MSTORE MSTORE8 SLOAD SSTORE MSIZE PUSHx DUPx SWAPx
Process flow operations(流程)控制流程指令STOP JUMP JUMPI PC JUMPDEST
System operations(系統)給執行該程式的系統用的 opcodeLOGx CREATE CALL CALLCODE RETURN DELEGATECALL STATICCALL REVERT INVALID SELFDESTRUCT
Logic operations(邏輯)比較與位元邏輯LT GT SLT SGT EQ ISZERO AND OR XOR NOT BYTE
Environmental operations(環境)處理執行環境資訊GAS ADDRESS BALANCE ORIGIN CALLER CALLVALUE CALLDATALOAD CALLDATASIZE CALLDATACOPY CODESIZE CODECOPY GASPRICE EXTCODESIZE EXTCODECOPY RETURNDATASIZE RETURNDATACOPY
Block operations(區塊)取得當前區塊的資訊BLOCKHASH COINBASE TIMESTAMP NUMBER DIFFICULTY GASLIMIT

幾個值得記住的語意差異(書中註解原文):

  • REVERT:中止執行、回復狀態變更,但仍回傳資料與剩餘的 gas
  • INVALID:指定的無效指令。
  • SELFDESTRUCT:中止執行並把帳戶登記為待刪除。
  • DELEGATECALL:用「別的帳戶的程式碼」對本帳戶做訊息呼叫,但 sender 與 value 沿用當前值
  • GAS:取得可用的 gas 量(已扣掉本指令的部分)。
  • BLOCKHASH:只拿得到最近 256 個已完成區塊的雜湊。

5. Turing completeness and gas:gas 就是停機問題的解

這一節是整章的中心論證。

“This capability, however, comes with an very important caveat: some programs take forever to run. An important aspect of this is that we can’t tell, just by looking at a program, whether it will take forever or not to execute. … This is called the halting problem and would be a huge problem for Ethereum if it were not addressed.”
(譯文:然而這個能力帶著一個非常重要的但書:有些程式會永遠跑不完。其中很關鍵的一點是,我們無法光看一支程式就判斷它會不會永遠執行下去。……這就是所謂的停機問題,若不加以處理,對以太坊會是個大麻煩。)
—— Ch.13 “Turing Completeness and Gas”(PDF p.566)

為什麼對以太坊特別致命:前面說過它像一台沒有排程器的單執行緒機器,一旦卡進無窮迴圈就整台不能用了。

解法:

“However, with gas, there is a solution: if after a prespecified maximum amount of computation has been performed, the execution hasn’t ended, the execution of the program is halted by the EVM. This makes the EVM a quasi–Turing-complete machine: it can run any program you feed into it, but only if the program terminates within a particular amount of computation.”
(譯文:然而有了 gas 就有解:如果在執行完預先指定的最大運算量之後程式還沒結束,EVM 就會中止這支程式的執行。這使得 EVM 成為一台準圖靈完備(quasi–Turing-complete)的機器:你餵給它的任何程式它都能跑,但前提是這支程式要在某個特定的運算量之內終止。)
—— Ch.13 “Turing Completeness and Gas”(PDF p.566)

同樣的話在章首出現過一次(“What Is the EVM?”,p.543):EVM 是準圖靈完備的狀態機,「quasi」是因為所有執行過程都被該次合約執行可用的 gas 量限制在有限步數內;因此停機問題「被解決了」(所有程式執行都會停),而執行可能(不論意外或惡意)永遠跑下去、進而讓整個以太坊平台停擺的情況也被避免了。

書特別註明那個上限不是固定的:你可以付錢把它提高,最高到「block gas limit」,而大家也可以隨時間一起同意調高那個上限;但在任一時刻上限都存在,執行時消耗太多 gas 的交易會被中止。

6. gas 的角色與計費

gas 是什麼(“Gas”,p.567):

“Gas is Ethereum’s unit for measuring the computational and storage resources required to perform actions on the Ethereum blockchain. In contrast to Bitcoin, whose transaction fees only take into account the size of a transaction in kilobytes, Ethereum must account for every computational step performed by transactions and smart contract code execution.”
(譯文:gas 是以太坊用來衡量「在以太坊區塊鏈上執行動作所需的運算與儲存資源」的單位。相對於比特幣——它的交易手續費只考慮交易以 KB 計的大小——以太坊必須為交易與智能合約程式碼執行所進行的每一個運算步驟記帳。)
—— Ch.13 “Gas”(PDF p.567)

書從 Yellow Paper 舉的三個例子:兩數相加 3 gas算一次 Keccak-256 是 30 gas + 每 256 bits 被雜湊的資料再加 6 gas送出一筆交易 21,000 gas

為什麼需要 gas——書明講是「雙重角色」(dual role):

  1. 當作波動的以太幣價格礦工工作報酬之間的緩衝;
  2. 當作對阻斷服務攻擊(DoS)的防禦。為了防止意外或惡意的無窮迴圈及其他網路運算浪費,每筆交易的發起者都必須為「自己願意付費的運算量」設一個上限。gas 制度因此讓攻擊者無法免費發垃圾交易——他們必須按所消耗的運算、頻寬與儲存資源等比例付錢。

執行期間怎麼記帳(“Gas Accounting During Execution”,p.568):

  • EVM 一開始拿到的 gas supply = 交易中指定的 gas limit。
  • 每個被執行的 opcode 都有 gas 成本,EVM 一步步走、供給量一路減少。
  • 每個操作之前,EVM 會先檢查 gas 夠不夠付這個操作;不夠就中止執行、交易回滾。
  • 成功跑完(沒耗盡 gas):用掉的 gas 成本按交易指定的 gas price 換成 ether 付給礦工當手續費。
miner fee = gas cost * gas price
remaining gas = gas limit - gas cost
refunded ether = remaining gas * gas price
  • out of gas:操作立即終止,拋出 out of gas 例外,交易回滾、所有狀態變更被撤銷。

“Although the transaction was unsuccessful, the sender will be charged a transaction fee, as miners have already performed the computational work up to that point and must be compensated for doing so.”
(譯文:雖然這筆交易失敗了,發送者仍會被收取交易手續費,因為礦工已經執行了到那個時點為止的運算工作,必須獲得補償。)
—— Ch.13 “Gas Accounting During Execution”(PDF p.568)

gas cost 與 gas price 是兩回事(“Gas Cost Versus Gas Price”,p.570)——書自己做的 recap:

  • Gas cost:執行某個特定操作所需的 gas「單位數」。
  • Gas price:你送交易到以太坊網路時,願意為「每一單位 gas」支付的 ether 金額。
transaction fee = total gas used * gas price paid   (in ether)

發送者指定自己願付的 gas price,讓市場去決定以太幣價格與運算成本(以 gas 計)之間的關係;礦工在組新區塊時可以從待處理交易中挑出價較高的,所以出高一點的 gas price 會誘使礦工把你的交易納入、更快確認。實務上發送者設的 gas limit 會大於或等於預期用量,設得比實際消耗高的話多的部分會退還,因為礦工只為實際做的工作獲得補償。

書還附了一個容易搞混的提醒:

“While gas has a price, it cannot be “owned” nor “spent.” Gas exists only inside the EVM, as a count of how much computational work is being performed.”
(譯文:gas 雖然有價格,但它無法被「擁有」也無法被「花用」。gas 只存在於 EVM 之內,作為「執行了多少運算工作」的計數。)
—— Ch.13 “Gas Cost Versus Gas Price”(PDF p.570)

發送者被收取的是 ether 手續費,這筆錢被換算成 gas 供 EVM 記帳,最後再換回 ether 付給礦工。

退款機制:負的 gas 成本(“Negative gas costs”,p.571)。以太坊用「退還部分執行期間用掉的 gas」來鼓勵刪除不再需要的 storage 變數與帳戶,EVM 中有兩個負 gas 成本的操作:

操作退款
刪除一份合約(SELFDESTRUCT24,000 gas
把某個 storage 位址從非零值改成零(SSTORE[x] = 015,000 gas

“To avoid exploitation of the refund mechanism, the maximum refund for a transaction is set to half the total amount of gas used (rounded down).”
(譯文:為避免退款機制被濫用,一筆交易的最高退款額被設定為「所使用 gas 總量的一半」(無條件捨去)。)
—— Ch.13 “Negative gas costs”(PDF p.571)

block gas limit(“Block Gas Limit”,p.572):一個區塊中所有交易可消耗的 gas 上限,限制了一個區塊能塞進多少交易。書的例子:五筆交易 gas limit 分別為 30,000/30,000/40,000/50,000/50,000,若 block gas limit 是 180,000,其中任四筆可進同一個區塊,第五筆要等下一個區塊。礦工若想塞進一筆需要 gas 超過當前 block gas limit 的交易,整個區塊會被網路拒絕;多數以太坊客戶端會先給你「transaction exceeds block gas limit」的警告擋下來。

上限由誰決定:礦工集體決定。協定內建投票機制,每個區塊的礦工可以把 block gas limit 往任一方向調整 1/1,024(0.0976%),形成隨網路需求浮動的區塊大小;預設挖礦策略是投給「至少 4.7 million gas,但以近期每區塊平均總用量的 150% 為目標(用 1,024 個區塊的指數移動平均)」。

7. 額外:dispatcher 為什麼要檢查 calldata 有沒有 4 bytes

Ch.13 的 “Disassembling the Bytecode”(p.560–565)把 Faucet.sol 的 runtime bytecode 拆開看,開頭那段就是 dispatcher:讀進交易的 data 欄位,把相關部分送到對應的 function。

它做的第一件事是 PUSH1 0x4 / CALLDATASIZE / LT / JUMPI——檢查 calldata 是不是少於 4 bytes。原因是 function identifier 的機制:

“Each function is identified by the first 4 bytes of its Keccak-256 hash.”
(譯文:每個函式都由它的 Keccak-256 雜湊值的前 4 個位元組來識別。)
—— Ch.13 “Disassembling the Bytecode”(PDF p.560)

keccak256("withdraw(uint256)") = 0x2e1a7d4d…,所以 withdraw(uint256) 的 identifier 就是 0x2e1a7d4d。identifier 固定 4 bytes,因此若整個 data 欄位不足 4 bytes,就不可能對應到任何 function——除非有定義 fallback function。Faucet.sol 有實作 fallback,所以 calldata 少於 4 bytes 時 EVM 就跳到 fallback;如果沒實作 fallback,合約會直接丟出例外。(編碼那一側在 工具-以太坊帳戶與交易。)

順帶一個部署時的坑(“Contract Deployment Code”,p.557–558):建立合約的交易 to 填特殊的 0x0 位址、data 放初始化程式碼;新合約帳戶的程式碼不是 data 欄位裡的東西,而是「那段部署程式碼執行後的輸出」。所以 solc --bin(deployment bytecode)與 solc --bin-runtime(runtime bytecode)不同,而 runtime bytecode 完整包含在 deployment bytecode 之內

🧪 我實際套用的紀錄

  • 2026-07-15:(待填)

⚠️ 注意 / 什麼時候不適用

  • 本卡的 gas 機制是本書(2018)當時的單一 gasPrice 市場模型:發送者自己指定一個 gas price,礦工挑高價的先打包,手續費全額付給礦工(transaction fee = total gas used * gas price paid)。書裡就是這樣寫的,本卡完全不做現代化改寫(見下方「刻意不寫的東西」)。
  • 具體 gas 數字要查 Appendix C。 本章正文只給了 ADD 3 gas、SHA3 30 gas(+ 每 256 bits 資料 6 gas)、一筆交易 21,000 gas,以及兩筆退款(24,000 / 15,000)。其餘一律指向 Appendix C 的 Table C-1,本卡不代填。
  • gas 定價本身就出過事:2016 年有攻擊者找到並利用了「gas 成本與真實資源成本不匹配」的漏洞,製造出運算極貴的交易,讓以太坊主網幾乎停擺;後來以 Tangerine Whistle 硬分叉調整了相對 gas 成本才解決(“Gas Accounting Considerations”,p.569)。「gas 表是對的」不是天經地義的假設。
  • 失敗也照收費,而且狀態全部回滾——你付的是礦工已經做掉的運算,不是結果。
  • 退款有上限:最多退到該筆交易總用量的一半(無條件捨去),所以不要指望靠大量 SSTORE[x]=0 把成本壓到接近零。
  • 子呼叫的 gas 是被分配的:合約呼叫合約時,每一層拿到的 gas 不能超過上一層剩餘量,所以子層可能單獨 OOG 而中止,執行回到上一層——這正是很多安全反模式的溫床,見 工具-智能合約安全反模式(例如只給 2300 gas 的 transfer、未檢查回傳值的 send、以及靠迴圈撐爆 block gas limit 的 DoS)。
  • 書中的 block gas limit 是「寫作當時」的 8 million gas(約可容納 380 筆 21,000 gas 的基本交易),這是 2018 年的快照,不是常數。
  • 這張卡不談交易欄位怎麼組gasPricegasLimit 這兩個欄位在交易結構裡的位置與簽章流程,見 工具-以太坊帳戶與交易

刻意不寫的東西

本書出版於 2018 年,以下都在本書之後,本卡一律不寫、也不用它們覆蓋書中說法

  • EIP-1559 的 base fee / priority fee(小費)/ base fee 燒毀機制——書中完全沒有這個概念。書寫的是單一 gasPrice、手續費全額付給礦工、礦工按價格高低排序挑交易。本卡照書寫。
  • The Merge / PoS——本章結尾說「接下來 Chapter 14 要看以太坊達成去中心化共識的機制」,書中脈絡是礦工(miners)、挖礦程式(Ethminer)、DIFFICULTY opcode、區塊受益者(block beneficiary)。本卡通篇沿用「礦工」。
  • Layer 2 / rollup / blob(EIP-4844) 等擴容方案——書談的擴容只有「礦工投票調整 block gas limit(每次 ±1/1,024)」。
  • 後來新增或改語意的 opcode(如 CHAINIDSELFBALANCEBASEFEEPUSH0SHL/SHR/SAR 位移指令、transient storage TLOAD/TSTORE)——上面的 7 類 opcode 表就是書中列的那些,一個沒加。
  • SELFDESTRUCT 退款已被移除、SSTORE 退款上限改成 1/5 等後續調整——本卡照書寫 24,000 / 15,000 與「一半」的上限。

🔗 相關工具

📜 合約

🎯 什麼情境該想到我

當你想理解或撰寫「部署到區塊鏈、被交易觸發後自動執行且公開可驗證的程式」時。

本卡只涵蓋 Ch.7 “Smart Contracts and Solidity”(PDF p.254–310)——合約的定義、生命週期、Solidity 的基本語言構件。安全反模式(重入、溢位、權限、搶跑…)全部在 工具-智能合約安全反模式(Ch.9),本卡不重複。

⚙️ 怎麼用(心法與注意)

1. 先把「smart contract」這個名字的誤導拆掉

Ch.7 “What Is a Smart Contract?”(PDF p.255)直接說這個詞在以太坊的脈絡下是個誤稱

“In the 1990s, cryptographer Nick Szabo coined the term and defined it as ‘a set of promises, specified in digital form, including protocols within which the parties perform on the other promises.’ … In the context of Ethereum, the term is actually a bit of a misnomer, given that Ethereum smart contracts are neither smart nor legal contracts, but the term has stuck.”
—— 譯文:1990 年代,密碼學家 Nick Szabo 造出這個詞,把它定義為「一組以數位形式指定的承諾,包含各方履行其他承諾所依循的協定」。……在以太坊的脈絡下,這個詞其實有點名不副實,因為以太坊的智能合約既不聰明、也不是法律合約,但這個名字就這麼沿用下來了。

書自己採用的定義是:

“…immutable computer programs that run deterministically in the context of an Ethereum Virtual Machine as part of the Ethereum network protocol — i.e., on the decentralized Ethereum world computer.”
—— 譯文:……在以太坊虛擬機的脈絡中、作為以太坊網路協定的一部分而決定性地執行的不可變電腦程式——也就是跑在去中心化的以太坊世界電腦上。

書把這句定義拆成五個字(p.255–256),這五個字就是這張卡的骨架:

  • Computer programs(電腦程式)——“The word ‘contract’ has no legal meaning in this context.”(譯文:「合約」這個詞在這裡沒有任何法律意義。)
  • Immutable(不可變)——一經部署,程式碼就改不了;唯一的修改方式是部署一個新的實例
  • Deterministic(決定性)——給定觸發它的那筆交易脈絡與當下的鏈上狀態,任何人執行的結果都相同。
  • EVM context(執行脈絡極窄)——合約只能取用自己的 state、呼叫它的那筆交易的脈絡、以及最近幾個區塊的部分資訊。
  • Decentralized world computer(去中心化世界電腦)——EVM 在每個節點各跑一份,但因為初始狀態相同、產出狀態也相同,整個系統對外表現得像一台電腦。

2. 合約帳戶沒有私鑰,它「自己擁有自己」

Ch.7 章首(PDF p.254)與 “Life Cycle of a Smart Contract”(p.257):合約帳戶同時擁有程式碼與資料儲存,而 EOA 兩者皆無;合約帳戶沒有私鑰,因此以其合約程式碼所預先規定的方式「控制自己」。

“As the contract creator, you don’t get any special privileges at the protocol level (although you can explicitly code them into the smart contract). You certainly don’t receive the private key for the contract account, which in fact does not exist — we can say that smart contract accounts own themselves.”
—— 譯文:身為合約的建立者,你在協定層面上不會拿到任何特權(不過你可以把特權明確寫進合約程式碼裡)。你當然也不會拿到合約帳戶的私鑰——那把私鑰根本不存在;我們可以說,智能合約帳戶擁有它自己

實務含意:你想要的任何管理員權限,都得自己寫進程式碼(見下面的 onlyOwner)。

3. 合約不會自己跑——一定是交易觸發的

“Life Cycle of a Smart Contract”(PDF p.257):

“Importantly, contracts only run if they are called by a transaction. … Contracts never run ‘on their own’ or ‘in the background.’ Contracts effectively lie dormant until a transaction triggers execution, either directly or indirectly as part of a chain of contract calls.”
—— 譯文:重要的是,合約只有在被交易呼叫時才會執行。……合約絕不會「自己跑」或「在背景跑」。合約實際上是休眠的,直到某筆交易直接或間接(作為一連串合約呼叫的一環)觸發它為止。

同一段還給了三個常被忽略的性質:

  • 任何執行鏈的第一個合約,一定是被某個 EOA 發起的交易呼叫的。
  • 世界電腦可視為單執行緒機器,合約不會「平行」執行。
  • 交易是原子的:不管它呼叫了幾個合約,全部成功才寫入狀態;中途出錯則所有狀態變更「回滾」(rolled back),彷彿沒發生過——但失敗仍被記錄,且 gas 照扣

4. 「不可變」是真的;「刪不掉」是錯的

這是舊版這張卡最需要修正的地方。程式碼確實改不了,但合約可以被刪除——前提是作者事先把 selfdestruct 寫進去(p.258、p.283–284):

“Note that you must explicitly add this command to your contract if you want it to be deletable — this is the only way a contract can be deleted, and it is not present by default. In this way, users of a contract who might rely on a contract being there forever can be certain that a contract can’t be deleted if it doesn’t contain a SELFDESTRUCT opcode.”
—— 譯文:注意,如果你要讓合約可被刪除,必須明確把這道指令加進合約——這是刪除合約的唯一途徑,而且它預設不存在。如此一來,那些指望合約永遠存在的使用者,就能確定:只要合約裡沒有 SELFDESTRUCT opcode,它就刪不掉

補充幾點:SELFDESTRUCT(舊名 SUICIDE)會把合約的程式碼與 storage 從該位址移除,留下一個空帳戶;之後送進去的交易不會執行任何程式碼。這個操作耗「負 gas」(gas 退費),用來獎勵釋放節點的儲存資源。刪除不會抹掉該合約過去的交易歷史,因為區塊鏈本身是不可變的。selfdestruct(recipient) 要帶一個位址參數,用來接收合約剩餘的 ether。

5. 完整生命週期

把 p.257–258 與 p.283–284 串起來:

  1. 用高階語言(多半是 Solidity)寫 → 編譯成 EVM bytecode(solc --bin)。
  2. 用一筆合約建立交易部署:送到特殊的建立位址 0x0
  3. 合約位址由建立者帳戶 + nonce 推導而來,不是隨機的。
  4. 若有 constructor,它在建立交易中執行一次,用來初始化狀態,之後即被丟棄
  5. 此後合約休眠,等交易呼叫;ABI + 部署位址就是外界互動所需的全部(p.269:“All that is needed for an application to interact with a contract is an ABI and the address where the contract has been deployed.”)。
  6. 生命週期的另一端是 SELFDESTRUCT——如果作者寫了的話。

⚠️ 建構子在 v0.4.21 以前是「與合約同名的函式」,書明講這種寫法會出事:合約改名或拼錯,它就退化成一個任何人都能呼叫的普通函式,第三方可以在部署後把自己設成 owner 劫持合約。v0.4.22 起改用不具名的 constructor 關鍵字(p.283–284)。案例細節見 工具-智能合約安全反模式

6. 函式的兩組關鍵字:誰能呼叫 vs. 能做什麼

“Functions”(PDF p.280–282)給的宣告式:

function FunctionName([parameters]) {public|private|internal|external}
[pure|constant|view|payable] [modifiers] [returns (return types)]

第一組是可見性public(預設,合約 / EOA / 自身皆可呼叫)、external(像 public,但合約內部要呼叫得加 this.)、internal(僅合約內與繼承者)、private(像 internal,但繼承者也不能呼叫)。

這裡有一句必須記住的話(p.281):

“Keep in mind that the terms internal and private are somewhat misleading. Any function or data inside a contract is always visible on the public blockchain, meaning that anyone can see the code or data. The keywords described here only affect how and when a function can be called.”
—— 譯文:請記住,internalprivate 這兩個詞有點誤導。合約內的任何函式或資料在公開區塊鏈上永遠是可見的,意思是任何人都能看到程式碼或資料。這裡描述的關鍵字只影響一個函式如何、以及何時可以被呼叫。

第二組決定行為

  • viewconstant 是即將淘汰的別名)——承諾不修改任何狀態。書寫作當下編譯器只發警告、不強制,預期在 v0.5 才成為強制。
  • pure——既不讀也不寫 storage,只吃參數、吐回傳值;目的是鼓勵無副作用的宣告式風格。
  • payable——能接受入金;沒標 payable 的函式會拒收 ether(例外:coinbase 付款與 SELFDESTRUCT 繼承的 ether 仍會進來,因為那些付款本來就不執行程式碼)。

另外,每個合約可以有一個不具名的 fallback function,在沒有指名任何函式時被呼叫;它不能有參數、也不能有回傳值。

7. Function modifier:存取控制的基本樣式

“Function Modifiers”(PDF p.287–288)。_; 是佔位符,會被「被修飾函式」的程式碼替換掉,等於把 modifier 包在函式外面

modifier onlyOwner {
    require(msg.sender == owner);
    _;
}
 
function destroy() public onlyOwner {
    selfdestruct(owner);
}

“Function modifiers are an extremely useful tool because they allow us to write preconditions for functions and apply them consistently, making the code easier to read and, as a result, easier to audit for security.”
—— 譯文:函式修飾詞是極其有用的工具,因為它讓我們能為函式寫下前置條件並一致地套用,使程式碼更易讀,因而也更容易做安全稽核

書順帶提醒:判斷 owner 時不要用 tx.origin——那會讓惡意合約在你不知情下摧毀你的合約(p.286)。「為什麼」在 Ch.9,見 工具-智能合約安全反模式

書接著用繼承(contract Child is Parent,支援多重繼承)把 owned / mortal 抽成基底合約,理由同樣是模組化與可稽核性(p.289–290)。

8. 錯誤處理:require 守輸入,assert 守內部

“Error Handling (assert, require, revert)“(PDF p.291–292):出錯時所有狀態變更會沿著整條呼叫鏈回滾,這正是交易原子性的來源。慣例是——

“By convention, assert is used when the outcome is expected to be true, meaning that we use assert to test internal conditions. By comparison, require is used when testing inputs (such as function arguments or transaction fields), setting our expectations for those conditions.”
—— 譯文:依慣例,assert 用在「結果理應為真」的情況,也就是用它來檢驗內部條件;相對地,require 用來檢驗輸入(例如函式參數或交易欄位),為那些條件設定我們的預期。

throw 已淘汰,改用 revert。v0.4.22 起 require / revert 可以帶一段錯誤訊息,會記進交易 log。書明白指出這是個取捨:多寫檢查會稍微增加 gas 消耗,測試網合約值得多報錯,主網合約可能選擇省 gas(p.292)。

9. Events:合約唯一的對外廣播管道

“Events”(PDF p.293–294):交易完成(成功或失敗)會產生 transaction receipt,其中的 log entries 記錄執行期間發生的事,而 events 就是產生這些 log 的 Solidity 高階物件

“Events are especially useful for light clients and DApp services, which can ‘watch’ for specific events and report them to the user interface, or make a change in the state of the application to reflect an event in an underlying contract.”
—— 譯文:Events 對輕節點與 DApp 服務特別有用:它們可以「監看」特定事件,回報到使用者介面,或改變應用程式的狀態以反映底層合約中發生的事。

用法:event Withdrawal(address indexed to, uint amount); 宣告,用 emit Withdrawal(msg.sender, withdraw_amount); 發出。參數前加 indexed 會讓該值進入可搜尋/過濾的索引表——設計事件時就要先想好前端要用什麼條件查

10. Gas 是寫法的約束條件,不是事後才煩惱的事

“Gas Considerations”(PDF p.304–307)。超過 gas limit 時的三件事:拋出 out of gas 例外 → 狀態回滾到執行前 → 付出去的 ether 當作交易手續費,不退。因為 gas 由發起交易的使用者付,高 gas 成本的函式會勸退使用者,所以壓低 gas 成本是寫合約的人的責任。書給兩條具體戒律:

  • Avoid Dynamically Sized Arrays(避免動態大小陣列)——任何走訪動態陣列的迴圈都有 gas 用爆的風險,可能在找到答案前就耗盡,白花時間與 ether 卻毫無結果。
  • Avoid Calls to Other Contracts(避免呼叫其他合約)——對方函式的 gas 成本未知就有 OOG 風險。並且:“Avoid using libraries that are not well tested and broadly used. The less scrutiny a library has received from other programmers, the greater the risk of using it.”(譯文:避免使用未經充分測試、未被廣泛使用的函式庫。一個函式庫受到其他程式設計者的檢視越少,用它的風險就越大。)

估算方式是 contract.myMethod.estimateGas(...)。書提醒這只是估計——EVM 是圖靈完備的,同一個函式不同呼叫路徑的 gas 可能天差地遠。書的建議是把評估函式 gas 成本納入開發流程,避免部署到主網時才驚訝。細節見 工具-Gas與EVM

11. 型別上的兩個小習慣

“Data Types”(PDF p.273–274):uint 不帶尺寸後綴時預設是 256 位元,以匹配 EVM 的字長。書示範的可讀性改善很值得抄:用單位後綴取代裸數字——

require(withdraw_amount <= 100000000000000000);   // 不易讀
require(withdraw_amount <= 0.1 ether);            // 好讀

時間單位(seconds / minutes / hours / days)與 ether 單位(wei / finney / szabo / ether)都能當後綴用。

12. 呼叫其他合約:三種方式,風險遞增

“Calling Other Contracts”(PDF p.297–300):

“When writing smart contracts, you must keep in mind that while you may mostly expect to be dealing with EOAs, there is nothing to stop arbitrarily complex and perhaps malign contracts from calling into and being called by your code.”
—— 譯文:寫智能合約時你必須謹記:雖然你多半預期打交道的對象是 EOA,但沒有任何機制能阻止任意複雜、可能懷有惡意的合約呼叫你的程式、或被你的程式呼叫

  • new 自己建立實例——最安全,因為你確知它的介面與行為。
  • 把既有位址轉型成合約型別——風險高得多;那個位址上的 withdraw 可能跟你宣告的完全不是同一回事。
  • 低階 call / delegatecall——最靈活也最危險,等於在合約脈絡裡構造一筆原始交易;書在這裡直接把讀者指向 Ch.9 的重入攻擊。delegatecall 的差別是不改變 msg 脈絡msg.sender 維持原樣),常用來呼叫函式庫,讓別處的程式碼操作自己的 storage。

🧪 我實際套用的紀錄

  • 2026-07-15:(待填)

⚠️ 注意

  • 「合約」在這裡沒有法律意義。 書明說 “The word ‘contract’ has no legal meaning in this context.”——別把它當成一份可以上法院的契約,它就是一段照字面執行的程式。
  • 不可變 + 管錢 = 容錯空間極小。 書在講語言選擇時的原話是 “In smart contracts, bugs literally cost money.”(譯文:在智能合約裡,bug 是真的會花掉錢。)——這也是為什麼宣告式語言在合約領域比在一般軟體重要得多,即使實際上最流行的 Solidity 是命令式的(p.260–261,“Programmers, like most humans, resist change!”)。
  • 本卡反映的是 2018 年、Solidity v0.4.24 時代的內容(書自述撰寫當時版本為 0.4.24,並說 0.5 即將發布)。範例中的 this.balance、同名建構子、throwcallcode 等寫法都屬於那個年代。書出版之後的語言演進、工具鏈與部署慣例不在本卡範圍內,實際動工前請回查現行官方文件。
  • 合約安全的完整清單不在這裡。 重入、算術溢位、預設可見性、tx.origin 授權、搶跑等等都在 Ch.9,見 工具-智能合約安全反模式

🔗 相關工具

  • 工具-智能合約安全反模式 —— 同一本書 Ch.9;本卡刻意不寫的安全細節(重入、溢位、權限、DELEGATECALL、搶跑…)全在那張,寫任何管錢的合約前必讀
  • 工具-以太坊帳戶與交易 —— 合約帳戶與 EOA 的差別、以及「合約只有被交易呼叫才會執行」的另一半:交易本身長什麼樣
  • 工具-Gas與EVM —— 本卡第 10 點的展開:gas 怎麼計價、EVM 怎麼執行 bytecode
  • 工具-預言機 —— 合約的執行脈絡極窄(只看得到自己的 state 與最近的區塊),要拿鏈外資料就得靠預言機
  • 工具-代幣標準與DApp —— 合約最常見的實際用途,看代幣/NFT/DApp 怎麼用合約組起來
  • 工具-用例外處理錯誤 —— 本卡第 8 點(require 守輸入、assert 守內部)的通用工程版本
  • 工具-防禦式編程 —— 「假設輸入都是惡意的」那套基本功;合約版的展開在 Ch.9 的 Security Best Practices
  • 精通以太坊 —— 本卡來源(Ch.7 “Smart Contracts and Solidity”,PDF p.254–310)

🎯 什麼情境該想到我

當你「要把一份會管錢的合約丟上鏈,想先照著一份清單把已知的坑掃過一遍」的時候。

這張是 工具-智能合約 的安全展開:那張講「合約是什麼、為什麼不可變」,這張講「不可變 + 管錢 + 全公開,會被人怎麼玩死」。

書中 Ch.9 的結構固定是三段——vulnerability(漏洞怎麼成立)→ attack example(怎麼打)→ preventative techniques(怎麼防),讀的時候照這個節奏走。

為什麼合約安全特別難

Ch.9 章首(PDF p.328)給出四個理由:

“As with other programs, a smart contract will execute exactly what is written, which is not always what the programmer intended. Furthermore, all smart contracts are public, and any user can interact with them simply by creating a transaction. Any vulnerability can be exploited, and losses are almost always impossible to recover.”
—— 譯文:跟其他程式一樣,智能合約會完全照著寫的執行,而那不見得是寫的人想要的。此外,所有智能合約都是公開的,任何使用者只要發一筆交易就能與之互動。任何漏洞都會被利用,而損失幾乎總是無法追回

四點連起來:照字面執行 → 全公開 → 誰都能互動 → 出事救不回來。這就是為什麼合約要用「航太工程等級」的嚴謹度寫。

⚙️ 怎麼用

第一層:防禦性設計五原則(Ch.9 “Security Best Practices”, PDF p.329–330)

書把 defensive programming 拆成五條,寫合約前先當檢查表:

  1. Minimalism/simplicity(極簡)

    “Complexity is the enemy of security. The simpler the code, and the less it does, the lower the chances are of a bug or unforeseen effect occurring.”
    —— 譯文:複雜度是安全的敵人。程式越簡單、做的事越少,出現 bug 或非預期效果的機會就越低。

    書講得很直白:如果有人跟你說他們的合約寫了「好幾千行程式碼」,你就該懷疑那個專案的安全性。Simpler is more secure.

  2. Code reuse(重用)——別重造輪子,遵守 DRY(見 工具-DRY原則)。書特別點名 “Not Invented Here” syndrome:想自己重寫一遍來「改進」某個元件時,安全風險通常大於改進的價值。

  3. Code quality(程式品質)

    “Writing DApps in Solidity is not like creating a web widget in JavaScript. Rather, you should apply rigorous engineering and software development methodologies, as you would in aerospace engineering or any similarly unforgiving discipline.”
    —— 譯文:用 Solidity 寫 DApp 不像用 JavaScript 做一個網頁小工具。你應該套用嚴謹的工程與軟體開發方法論,就像做航太工程或任何同樣不容出錯的領域那樣。

  4. Readability/auditability(可讀 / 可稽核)——合約是公開的,任何人都能讀 bytecode、也能反編譯。既然躲不掉,就乾脆公開開發、用開源協作的方式借社群的集體智慧。

  5. Test coverage(測試覆蓋)——合約跑在公開執行環境,任何人可以餵任何輸入。

    “You should never assume that input, such as function arguments, is well formed, properly bounded, or has a benign purpose.”
    —— 譯文:你絕不該假設輸入(例如函式參數)格式良好、範圍正確、或抱著善意。

第二層:七個著墨最深的反模式

1. Reentrancy 重入(Ch.9 “Reentrancy”, PDF p.332–339)

漏洞:合約把 ether 送到一個未知位址時,對方的 fallback function 會被觸發,而那段惡意程式碼可以再呼叫回原合約——路徑「reenter(重入)」了。

書的 EtherStore 範例中,withdrawFunds 先送錢(第 17 行 msg.sender.call.value(...))、之後才扣 balances(第 18 行)。攻擊合約在 fallback 裡再呼叫一次 withdrawFunds,此時餘額還沒被扣,所有 require 都照樣通過,於是遞迴領錢,一筆交易內把金庫領到只剩 1 ether

防法(書給三招,用一招即可,範例是為了示範才三招全上):

  • 用內建的 transfer:只給 2300 gas,不夠對方再去呼叫另一個合約。
  • checks-effects-interactions pattern:所有改 state 的邏輯都放在送錢/外部呼叫之前;對未知位址的外部呼叫要是該段程式的最後一個動作
  • mutex:用一個 state variable 在執行期間鎖住合約,擋掉重入呼叫。

真實案例:The DAO

“The DAO (Decentralized Autonomous Organization) attack was one of the major hacks that occurred in the early development of Ethereum. At the time, the contract held over $150 million. Reentrancy played a major role in the attack, which ultimately led to the hard fork that created Ethereum Classic (ETC).”
—— 譯文:DAO(去中心化自治組織)攻擊是以太坊早期發展中的重大駭客事件之一。當時該合約持有超過 1.5 億美元。重入在這次攻擊中扮演了主要角色,最終導致了那次催生出 Ethereum Classic(ETC)的硬分叉。

2. Arithmetic Over/Underflows 算術溢位(PDF p.340–346)

漏洞:EVM 用定長整數型別

“The Ethereum Virtual Machine specifies fixed-size data types for integers. This means that an integer variable can represent only a certain range of numbers. A uint8, for example, can only store numbers in the range [0,255]. Trying to store 256 into a uint8 will result in 0.”
—— 譯文:EVM 為整數指定了固定大小的資料型別。這表示一個整數變數只能表示某個範圍的數字。例如 uint8 只能存 [0,255] 範圍內的數;試圖把 256 存進 uint8 會得到 0。

把定長變數想成環狀(書用里程表和 sin 加 2π 當比喻):uint8 的 0 減 1 會變成 255(underflow);加上 2^8 = 256 則原地不動。

書的兩個範例:

  • TimeLock(時間鎖金庫):攻擊者拿到私鑰後,讀出公開的 lockTime,呼叫 increaseLockTime(2^256 - userLockTime) 讓它溢位歸零,直接把鎖打開領走 100 ether。
  • Ethernaut 的 Tokenrequire(balances[msg.sender] - _value >= 0) 這行永遠成立——uint256 減出來的結果不可能為負,只會 underflow 成巨大正數。零餘額的人可以憑空 transfer 出代幣。

防法:用數學函式庫取代標準的 + - * 運算子(除法排除,因為不會溢位、且 EVM 除以 0 會 revert)。書明確推薦 OpenZeppelin 的 SafeMath,並示範改寫成 balances[msg.sender].add(msg.value) 的版本。

真實案例:PoWHC(Proof of Weak Hands Coin)被 underflow 搬走 866 ether;一批 ERC20 合約的 batchTransfer() 溢位漏洞(CVE-2018-10299)。

3. Unexpected Ether 非預期的 ether(PDF p.347–354)

漏洞:很多人以為合約只能透過 payable 函式收 ether。錯。有兩種方式能強制把 ether 塞進合約而不執行任何程式碼

  • selfdestruct(自毀):任何合約都能呼叫 selfdestruct(target),把餘額全數送到 target;即使 target 沒有任何 payable 函式,fallback 也不會被呼叫
  • Pre-sent ether(預先送款):合約位址是決定性的——address = sha3(rlp.encode([account_address, transaction_nonce])),所以任何人都能在合約還沒部署前算出位址並先打錢進去,合約一誕生就帶著非零餘額。

因此把「合約餘額」當作 invariant(不變量)來檢查是危險的。

“The smoking gun for this vulnerability is the (incorrect) use of this.balance.”
—— 譯文:這個漏洞的確鑿證據就是(錯誤地)使用 this.balance

書的 EtherGame 範例:玩家每次投 0.5 ether,達到 3 / 5 / 10 ether 里程碑時給獎。攻擊者只要 selfdestruct 硬塞 0.1 ether,餘額就永遠不會是 0.5 的倍數,所有里程碑條件永遠不成立;更狠的是塞到超過 finalMileStoneclaimRewardrequire 永遠 revert,獎金永久鎖死

防法:合約邏輯不要依賴 this.balance 的精確值。若需要精確追蹤入金,自己宣告一個變數(如 depositedWei)在 payable 函式中累加——強制塞入的 ether 影響不到它。

4. DELEGATECALL(PDF p.355–363)

漏洞DELEGATECALLCALL 幾乎相同,差別在目標位址的程式碼是在呼叫端的 context 中執行msg.sendermsg.value 維持不變。這讓函式庫成為可能,但也代表——

“It is also important to notice that when we say that delegatecall is state-preserving, we are not talking about the variable names of the contract, but rather the actual storage slots to which those names point.”
—— 譯文:還有一點很重要:當我們說 delegatecall 會保留狀態時,指的不是合約的變數名稱,而是那些名稱所指向的實際 storage slot

State variable 是依宣告順序依序放進 slot。書的 Fibonacci 範例中,函式庫的 start 在 slot[0]、calculatedFibNumber 在 slot[1];但呼叫端 FibonacciBalance 的 slot[0] 是 fibonacciLibrary(函式庫位址)。於是函式庫的 setStart(x) 實際上改的是呼叫端的函式庫位址——攻擊者把自己的合約位址轉成 uint 傳進去,就把整個合約劫持了。

防法:用 Solidity 的 library 關鍵字實作函式庫——這保證函式庫是 stateless 且不可自毀的,避開 storage context 的複雜性。使用 DELEGATECALL 時要同時考慮函式庫合約與呼叫合約兩邊的 calling context。

真實案例:Parity Multisig Wallet 第二次駭事件WalletLibrary 本身也是一個合約、有自己的 state。有使用者直接對函式庫合約本身呼叫 initWallet 成為 owner,接著呼叫 kill 讓它自毀。所有指向這個函式庫的 Wallet 合約都沒有更換參照的方法,於是——

“As a result, all ether in all Parity multisig wallets of this type instantly became lost or permanently unrecoverable.”
—— 譯文:結果,所有這類 Parity 多簽錢包裡的 ether 瞬間全部遺失、永久無法取回。

5. Default Visibilities 預設可見性(PDF p.364–368)

漏洞:Solidity 的函式預設是 public。開發者若漏寫可見性修飾詞,本該是 private 的內部函式就對全世界開放。

書的 HashForEther 範例:_sendWinnings()(照命名慣例是內部函式)沒寫可見性 → 預設 public → 任何位址都能直接呼叫它把獎金領走,完全繞過 withdrawWinnings 的中獎條件檢查。

防法永遠明確標註每個函式的可見性,即使它本來就是 public。書提到當時較新版的 solc 已會對沒有明確可見性的函式發出警告。

真實案例:Parity Multisig Wallet 第一次駭事件,約 3100 萬美元的 ether 被偷。initMultiownedinitWallet 兩個函式都沒寫可見性、預設 public,攻擊者直接對已部署的合約呼叫它們,把 owner 重設成自己的位址,然後把錢包提空。

6. Unchecked CALL Return Values 未檢查的呼叫回傳值(PDF p.385–388)

漏洞callsend 失敗時不會 revert,只是回傳 false。開發者若以為失敗會自動 revert 而不去檢查回傳值,state 就會跟事實脫節。

書的 Lotto 範例:winner.send(winAmount); 沒檢查回傳值,下一行就把 payedOut = true。中獎者若因 gas 不足、或本身是個會在 fallback 中 throw 的合約而收款失敗,payedOut 照樣變成 true——接著任何人都能透過 withdrawLeftOver 把獎金領走。

防法:能用 transfer 就用 transfer(外部交易 revert 時它會跟著 revert);非用 send 不可就一定檢查回傳值。更穩健的做法是採用 withdrawal pattern(提領模式):把送錢邏輯隔離成一個獨立的 withdraw 函式,由使用者自己來領,把「交易可能失敗」的負擔轉移到呼叫者身上。

真實案例:Etherpot 樂透(winner.send(subpot) 沒檢查、下一行就標記已付款);更嚴重的版本發生在 King of the Ether。

7. Race Conditions / Front Running 競態與搶跑(PDF p.389–394)

漏洞:交易先進 pool、由礦工挑選並打包,通常按 gasPrice 排序。攻擊者可以盯著交易池,看到有價值的交易後,複製它的資料、用更高的 gasPrice 送出自己的版本,讓自己的交易排在原始交易前面。

書的 FindThisHash 範例:合約放著 1000 ether 懸賞某個雜湊的原像。使用者算出答案 Ethereum! 並送出交易,攻擊者在池中看到、驗證有效、用高得多的 gasPrice 重送——賞金被搶走,真正解題的人一無所獲

“Keep in mind that in this type of ‘front-running’ vulnerability, miners are uniquely incentivized to run the attacks themselves (or can be bribed to run these attacks with extravagant fees). The possibility of the attacker being a miner themselves should not be underestimated.”
—— 譯文:請記住,在這類「搶跑」漏洞中,礦工有獨特的誘因親自執行攻擊(或可用誇張的手續費賄賂他們去執行)。不要低估攻擊者本身就是礦工的可能性。

防法

  • gasPrice上限——只擋得住第一類攻擊者(一般使用者),礦工照樣能任意排序區塊內交易。
  • 更穩健的是 commit–reveal scheme:先送出含隱藏資訊(通常是雜湊)的交易,等它被打包後再送第二筆交易揭露內容。礦工和使用者都無從得知內容,兩類攻擊者都擋得住。但它藏不住交易金額——ENS 的做法是讓 commit 的資料包含願付金額、交易送任意值,reveal 階段再退還差額。

真實案例ERC20 的 approve——Alice 想把 Bob 的額度從 100 改成 50,Bob 盯著鏈、搶先花掉原本的 100,等 Alice 的交易生效後額度變 50,Bob 實際拿到 150。另一例是 Bancor:代幣價格由交易金額決定,任何人都能盯著交易池搶跑套利。

第三層:其餘反模式(書中列名,需要時回去讀)

反模式一句話PDF
Entropy Illusion(熵的幻覺)以太坊是決定性狀態機,鏈上沒有隨機源;用未來/當前的 block 變數當亂數,礦工可操控(挖到不利的區塊就丟掉重挖)。防法:commit–reveal、RandDAO、或中心化的 randomness oracle。p.369–372
External Contract Referencing(外部合約參照)Solidity 中任何位址都能被轉型成任何合約型別,不管那裡的程式碼是不是那個型別。範例:把 Rot13Encryption 換成什麼都不做的 Rot26Encryption 或直接 emit 明文的 Print。防法:用 new 在部署時建立實例、或硬編碼位址、或把位址設成 public 讓人可查。p.373–380
Short Address/Parameter Attack不是打合約,而是打與合約互動的第三方應用。ABI 編碼的參數若短了一個 byte,EVM 會在尾端補零,導致金額被乘以 256。防法:外部應用送上鏈前必須驗證所有輸入參數;參數順序也有影響(補零只發生在尾端)。p.381–384
Denial of Service (DoS)三種樣態:① 迴圈跑過外部可膨脹的陣列(攻擊者狂建帳號讓 for 迴圈 gas 超過區塊上限);② owner operations(特權帳號遺失私鑰 → 整個合約卡死);③ 靠外部呼叫推進狀態(對方合約拒收 ether → 永遠到不了下一個狀態)。防法:改用 withdrawal pattern、owner 改成多簽、加 time-lock 失效保護。p.395–399
Block Timestamp Manipulationblock.timestamp / now 可被礦工小幅調整。書的 Roulette 範例用 now % 15 == 0 判定中獎,池子夠大時礦工有誘因挑一個能中獎的 timestamp。防法:時間戳不可作為亂數來源;需要時間邏輯時改用 block.number 加平均出塊時間估算(BAT 的 ICO 就是這樣做)。p.400–403
Constructors with CareSolidity v0.4.22 之前,建構子是「與合約同名的函式」。合約改名而建構子沒跟著改(或拼錯),它就變成一個任何人都能呼叫的普通函式。真實案例 Rubixi(原名 DynamicPyramid,改名後建構子沒改,任何人都能把自己設成 creator 領走手續費)。已由 v0.4.22 引入的 constructor 關鍵字解決。p.404–407
Uninitialized Storage Pointers未初始化的區域 storage 變數會指向既有的 storage slot。書的 NameRegistrar 範例:函式內的 NameRecord newRecord; 預設落在 storage 且對應 slot[0],而 slot[0] 是 unlocked——傳一個尾端非零的 _name 就能把 unlocked 直接改成 true。防法:對複雜型別明確標註 memorystorage,並正視編譯器的警告。p.408–412
Floating Point and Precision書撰寫當時(v0.4.24)Solidity 不支援定點與浮點數,只能用整數自己刻。除法一律無條件捨去(賣 29 個代幣只拿到 2 ether)。防法:讓分子夠大(用 weiPerTokens 而非 tokensPerEth)、注意運算順序(先乘後除;Solidity 保證照書寫順序運算)、內部一律高精度、輸出時才降精度。可參考 DS-Math。p.413–416
Tx.Origin Authenticationtx.origin最初發起交易的帳號(穿透整個 call stack)。用它做授權會被釣魚:受害者只要送一筆交易給攻擊者的合約,其 fallback 就以受害者為 tx.origin 去呼叫 withdrawAll,錢全被領走。防法:tx.origin 不可用於授權;但 require(tx.origin == msg.sender) 是合法用途(用來擋掉中介合約的呼叫)。p.417–420

第四層:結論——最大化重用可信任的程式碼

“Perhaps the most fundamental software security principle is to maximize reuse of trusted code. In cryptography, this is so important it has been condensed into an adage: ‘Don’t roll your own crypto.’”
—— 譯文:也許最根本的軟體安全原則就是最大化重用可信任的程式碼。在密碼學中這件事重要到被濃縮成一句諺語:「別自己造密碼學。」

書在 “Contract Libraries” 一節推薦 OpenZeppelin suite(ERC20/ERC721 實作、各種 crowdsale 模型、Ownable / Pausable / LimitBalance 等常見行為,其中部分已成事實標準實作)、ZeppelinOS、以及套件管理系統 ethpm

🧪 我實際套用的紀錄

  • (待填)

⚠️ 注意 / 什麼時候不適用

  • ⚠️ 這份清單反映的是 2018 年的 Ethereum / Solidity 生態。 書中自己標明撰寫時的 Solidity 版本是 v0.4.24,所有範例都是那個年代的語法(constructor 之前的同名建構子、sha3throwsuicidevar i)。書出版之後的語言、工具鏈與協議變化一律不在本卡範圍內——要用在今天的專案前,務必回去查現行版本的官方文件與現行的最佳實務。
  • 書中明確提到「已被語言/工具層解掉」的只有這幾項(其餘不要自行推論):
    • Constructors with Care —— 書明說 “This issue has been addressed in version 0.4.22 of the Solidity compiler”,改用 constructor 關鍵字即可。
    • Default Visibilities —— 書說當時較新版的 solc 已會對未標可見性的函式發出警告(只是警告,不是強制)。
    • Uninitialized Storage Pointers —— 書說編譯器會發警告,且當時的 Mist(0.10)已不允許編譯這類合約。
    • 溢位(Arithmetic Over/Underflows):書中給的解法只有「用 SafeMath 這類函式庫」,沒有任何語言層內建檢查。 本書寫於那之前,所以這裡不宣稱任何「編譯器已內建」的說法。
  • 防範措施本身也有代價:書自己在 EtherStore 修正版註明「三招全用是不必要的,我們只是為了示範」——防重入挑一招即可,堆疊防禦會增加複雜度,而 complexity is the enemy of security
  • 「已審計」不等於安全。書在 External Contract Referencing 提醒:安全的合約也可能被用惡意的方式部署——稽核員公開驗證了程式碼,擁有者卻用一個指向惡意合約的位址去部署它,結果是一份「通過公開稽核」但有漏洞的合約。
  • Short Address Attack 不在合約層,是交易所/錢包等第三方應用的輸入驗證問題;只審 Solidity 程式碼掃不到它。
  • DoS 防範的中心化代價:書自己加註,加一個 maintenanceUser 來救 DoS 是可行的,但這類合約通常有信任問題,因為那個角色的權力太大。

🔗 相關工具

  • 工具-智能合約 —— 同一本書;那張講合約的基本模型與不可變性,這張是它的安全展開,先讀那張再讀這張
  • 工具-防禦式編程 —— 出自 程式碼大全;本章的 “Security Best Practices” 就是防禦式編程用在合約上的版本,通用工程原則的源頭在那裡
  • 精通以太坊 —— 本卡來源(Ch.9 “Smart Contract Security”)

🌐 應用層

🎯 什麼情境該想到我

當你要「發一個代幣、做 NFT,或是先決定這個專案到底該不該發幣」時。

本卡完全依據《Mastering Ethereum》(O’Reilly, 2018) Ch.10「Tokens」。

⚙️ 怎麼用

0. 先搞清楚「token」在講什麼

書從字源講起:「token」來自古英語 “tācen”,意思是記號或象徵,過去多半指私人發行、內在價值微不足道的類硬幣物件(公車代幣、洗衣代幣、電玩代幣)。而現在:

“Nowadays, “tokens” administered on blockchains are redefining the word to mean blockchain-based abstractions that can be owned and that represent assets, currency, or access rights.”
(譯文:如今,在區塊鏈上管理的「代幣」正在重新定義這個詞,使它意指「可被擁有、且代表資產、貨幣或存取權的區塊鏈式抽象物」。)
—— Ch.10 章首(PDF p.424)

書順帶點破那個「代幣=沒什麼價值」的既定印象從哪來:實體代幣通常被限制在特定商家、組織或地點,不易交換、通常只有單一功能。區塊鏈代幣把這些限制拿掉了——或者更準確地說,變成可重新定義的。(這個伏筆在後面的 utility token 段落會被回收。)

1. 一枚代幣可以代表什麼:10 種用途

“How Tokens Are Used”(p.425–426)明說貨幣只是第一個「app」,而且一枚代幣常常同時身兼數種功能:

用途書中舉例(譯文)
Currency(貨幣)價值由私下交易決定的一種貨幣形式
Resource(資源)分享經濟/資源共享環境中賺得或產出的資源,例如代表可在網路上共享的儲存或 CPU 的代幣
Asset(資產)內生或外生、有形或無形資產的所有權;黃金、不動產、汽車、石油、能源、MMOG 道具等
Access(存取權)對數位或實體財產的存取權,例如討論區、專屬網站、飯店房間、租車
Equity(股權)數位組織(如 DAO)或法律實體(如公司)的股東權益
Voting(投票權)數位或法律系統中的投票權
Collectible(收藏品)數位收藏品(如 CryptoPunks)或實體收藏品(如一幅畫)
Identity(身分)數位身分(如 avatar)或法律身分(如國民身分證)
Attestation(證明)由某個權威或去中心化聲譽系統做出的認證或事實證明,如結婚記錄、出生證明、大學學位
Utility(效用)用來存取或支付某項服務

書給的洞見是:實體世界裡這些功能常被綁死(駕照同時是「證明」也是「身分」,兩者分不開),而在數位領域,這些原本混在一起的功能可以被拆開、各自獨立發展(例如匿名的證明)。

2. 同質化(fungibility):ERC20 與 ERC721 的分水嶺

“Tokens are fungible when we can substitute any single unit of the token for another without any difference in its value or function.”
(譯文:當我們能用代幣的任一單位替換另一單位,而其價值或功能毫無差異時,這種代幣就是同質化的。)
—— Ch.10 “Tokens and Fungibility”(PDF p.427)

兩個容易被忽略的補充:

  • 嚴格說來,只要代幣的歷史來源(provenance)可被追蹤,它就不是完全同質化的——追蹤來源的能力會導致黑名單與白名單,進而削弱或消滅同質性。
  • 非同質化代幣各自代表一個獨特的有形或無形物件,因此不可互換(代表某幅梵谷的代幣不等同於代表畢卡索的代幣,即使兩者屬於同一套「藝術品所有權代幣」系統;某隻特定 CryptoKitty 也不能跟另一隻互換)。每個非同質化代幣都關聯到一個唯一識別碼,例如序號。

書另外用一個 NOTE 澄清:日常語境裡「fungible」常被拿來指「可直接兌換成錢」(賭場籌碼能兌現、洗衣代幣通常不能),這不是本書使用這個詞的意思

3. 交易對手風險與 intrinsicality:代幣背後有沒有人握著東西

Counterparty risk(p.428)是「交易的另一方未能履行義務的風險」。書的關鍵推論:當一項資產是透過「所有權代幣」間接交易時,保管人(custodian)會額外帶來交易對手風險——他們真的持有那項資產嗎?他們會不會承認(或允許)「代幣移轉=所有權移轉」?

接著是 intrinsicality(p.429)。「intrinsic」源自拉丁文 “intra”,意為「從內部而來」:

  • 內生(intrinsic)資產受共識規則管轄,跟代幣本身一樣。代表內生資產的代幣不帶額外的交易對手風險——你持有某隻 CryptoKitty 的金鑰,就沒有別人替你保管牠,你直接擁有它;持有私鑰=擁有資產,沒有中介。
  • 外生(extrinsic)資產(不動產、公司投票股份、商標、金條)的所有權由法律、習俗與政策管轄,與管轄代幣的共識規則分開,因此必然帶有額外的交易對手風險(保管人、外部登記、鏈外法規)。

“One of the most important ramifications of blockchain-based tokens is the ability to convert extrinsic assets into intrinsic assets and thereby remove counterparty risk.”
(譯文:以區塊鏈為基礎的代幣,最重要的後果之一,就是能把外生資產轉換成內生資產,藉此消除交易對手風險。)
—— Ch.10 “Tokens and Intrinsicality”(PDF p.429)

書舉的例子:從「公司股權(外生)」搬到「DAO 或類似組織中的股權/投票代幣(內生)」。

4. utility token vs equity token:這個專案該不該發幣

書把當時所有專案的發幣動機收斂成兩類,並直說這兩種角色常常被混為一談(“Using Tokens: Utility or Equity”,p.430):

定義(譯文)
Utility token使用該代幣是取得某項服務、應用或資源的必要條件。例如代表共享儲存等資源的代幣,或存取社群網路等服務的代幣
Equity token代表對某物(例如一家新創)的控制權或所有權的份額。可以窄到只是分配股利與利潤的無投票權股份,也可以寬到是 DAO 中的投票股份,由代幣持有者透過複雜治理系統管理平台

然後書開了一個以本章最直白的標題寫成的小節——“It’s a Duck!”(p.431):代幣是很棒的募資機制,但向公眾發行證券(股權)在多數司法管轄區是受監管的行為。許多新創把 equity token 偽裝成 utility token,希望繞過監管、把公開募資包裝成「服務存取憑證」的預售。書的判斷是:

“As the popular saying goes: “If it walks like a duck and quacks like a duck, it’s a duck.” Regulators are not likely to be distracted by these semantic contortions; quite the opposite, they are more likely to see such legal sophistry as an attempt to deceive the public.”
(譯文:如同那句俗諺:「如果牠走起來像鴨子、叫起來也像鴨子,那牠就是鴨子。」監管機關不太可能被這些語意上的扭曲所迷惑;恰恰相反,他們更可能把這種法律詭辯視為欺騙公眾的意圖。)
—— Ch.10 “It’s a Duck!”(PDF p.431)

接著是 “Utility Tokens: Who Needs Them?”(p.432–434),論證非常值得抄進產品決策清單:

  1. 每一項創新都是一道市場濾網。 你的新創本來就已經在走人跡罕至的路;再加一個 utility token、要求使用者為了用你的服務先去採用代幣,等於把濾網疊上第二層——你要求早期採用者同時採用兩種全新技術:你的產品,以及代幣經濟。
  2. 風險是疊加的。 加上 utility token,你就一併繼承了底層平台(以太坊)、更廣泛的經濟(交易所、流動性)、監管環境(股權/商品監管機關)與技術(智能合約、代幣標準)的所有風險。
  3. 你可能親手重建了「代幣=不值錢」的條件。 呼應章首的伏筆:實體代幣之所以價值微不足道,是因為只能用在很窄的脈絡(一家公車公司、一間洗衣店);如果你的代幣只能在你自己這個小市場的單一平台上使用,你就複製了同樣的條件。若使用者必須「把東西換成你的代幣→用掉→再把剩下的換回比較通用的東西」,你創造的其實是公司代用券(company scrip)。
  4. 但書也公平地給出反面:採用代幣同時繼承了整個代幣經濟的市場熱情、早期採用者、技術、創新與流動性——「問題在於好處與熱情是否勝過風險與不確定性」。

書的結論句:

“Adopt a token because your application cannot work without a token. Adopt it because the token lifts a fundamental market barrier or solves an access problem. Don’t introduce a utility token because it is the only way you can raise money fast and you need to pretend it’s not a public securities offering.”
(譯文:採用代幣,是因為你的應用少了代幣就無法運作;採用它,是因為代幣消除了某個根本性的市場障礙、或解決了某個存取問題。不要因為那是你唯一能快速募資的方式、而且你需要假裝那不是公開的證券發行,才去引進一個 utility token。)
—— Ch.10 “Utility Tokens: Who Needs Them?”(PDF p.433–434)

5. 代幣活在合約層,協定根本不知道它存在

這是理解後面所有 ERC20 怪癖的唯一前提

“Tokens are different from ether because the Ethereum protocol does not know anything about them. Sending ether is an intrinsic action of the Ethereum platform, but sending or even owning tokens is not. The ether balance of Ethereum accounts is handled at the protocol level, whereas the token balance of Ethereum accounts is handled at the smart contract level.”
(譯文:代幣不同於 ether,因為以太坊協定對它們一無所知。發送 ether 是以太坊平台的內建行為,但發送、甚至擁有代幣則不是。以太坊帳戶的 ether 餘額在協定層被處理,而代幣餘額則是在智能合約層被處理。)
—— Ch.10 “Tokens on Ethereum”(PDF p.435)

所以在以太坊上建立新代幣=部署一份新的智能合約;部署後由該合約處理一切,包含所有權、移轉與存取權。你可以隨心所欲地寫,但書說「照既有標準走大概是最明智的」。

6. ERC20:實際的介面定義

來歷(“The ERC20 Token Standard”,p.436):2015 年 11 月由 Fabian Vogelsteller 以 Ethereum Request for Comments 形式提出,被自動指派為 GitHub issue 編號 20,因而得名「ERC20 token」;後來成為 EIP-20,但大家仍多以原名稱呼。ERC20 是同質化代幣的標準,意思是同一種 ERC20 代幣的不同單位可互換、且沒有獨特屬性。

“The ERC20 standard defines a common interface for contracts implementing a token, such that any compatible token can be accessed and used in the same way.”
(譯文:ERC20 標準為實作代幣的合約定義了一套共通介面,使得任何相容的代幣都能以相同方式被存取與使用。)
—— Ch.10 “The ERC20 Token Standard”(PDF p.436)

必要 function 與 event(“ERC20 required functions and events”,p.436–437)——「一份符合 ERC20 的代幣合約至少必須提供下列 function 與 event」:

名稱書中定義(譯文)
totalSupply回傳目前存在的此代幣總單位數。ERC20 代幣可以是固定供給,也可以是可變供給
balanceOf給定一個地址,回傳該地址的代幣餘額
transfer給定地址與數量,從執行此次轉帳的地址餘額中,把該數量的代幣轉給該地址
transferFrom給定 sender、recipient 與數量,把代幣從一個帳戶轉到另一個帳戶。approve 搭配使用
approve給定接收方地址與數量,授權該地址從發出授權的帳戶中,執行多次、總額不超過該數量的轉帳
allowance給定 owner 地址與 spender 地址,回傳 spender 仍被核准可從 owner 提取的剩餘數量
Transfer(event)成功轉帳(呼叫 transfertransferFrom)時觸發的事件,即使是零金額的轉帳也會觸發
Approval(event)成功呼叫 approve 後記錄的事件

選用 function(p.437):name(人類可讀名稱,如 “US Dollars”)、symbol(人類可讀代號,如 “USD”)、decimals(代幣數量要除以幾位小數;若 decimals 為 2,代幣數量要除以 100 才是使用者看到的表示)。

書中給的 Solidity 介面原文(p.437–438):

contract ERC20 {
   function totalSupply() constant returns (uint theTotalSupply);
   function balanceOf(address _owner) constant returns (uint balance);
   function transfer(address _to, uint _value) returns (bool success);
   function transferFrom(address _from, address _to, uint _value) returns
      (bool success);
   function approve(address _spender, uint _value) returns (bool success);
   function allowance(address _owner, address _spender) constant returns
      (uint remaining);
 
   event Transfer(address indexed _from, address indexed _to, uint _value);
   event Approval(address indexed _owner, address indexed _spender, uint _value);
}

內部資料結構就兩張表(“ERC20 data structures”,p.438):

mapping(address => uint256) balances;                          // 誰有多少
mapping (address => mapping (address => uint256)) public allowed;  // 誰授權誰可花多少

每一次轉帳就是「從一個餘額扣掉、加到另一個餘額」。

兩種工作流程(“ERC20 workflows”,p.438–440):

  1. transfer:單筆交易。 錢包對錢包送代幣用的就是這條,絕大多數代幣交易都走這條。Alice 想送 10 顆給 Bob,她的錢包對代幣合約地址送一筆交易、呼叫 transfer(Bob, 10);合約調整 Alice 餘額(−10)與 Bob 餘額(+10),並發出 Transfer 事件。
  2. approve + transferFrom:兩筆交易。 讓代幣擁有者把控制權委派給另一個地址,最常見的用途是委派給一份合約來分發代幣,交易所也可以用。書的 ICO 範例:Alice 先部署 AliceCoin(全部發給自己)與 AliceICO 合約,再對 AliceCoin 呼叫 approve(AliceICO 地址, totalSupply 的 50%)(觸發 Approval 事件);此後 AliceICO 收到 Bob 的 ether,就按合約內的匯率呼叫 AliceCoin.transferFrom(Alice, Bob, 數量)只要不超過 Alice 設定的核准上限,AliceICO 可以無限次呼叫 transferFrom,並用 allowance 查自己還能賣多少。

實作要用現成的(“ERC20 implementations”,p.440–441):雖然 30 行左右的 Solidity 就能寫出相容的 ERC20,但實際實作都更複雜,因為要處理潛在的安全漏洞。EIP-20 標準提到兩份實作:Consensys EIP20(簡單易讀)與 OpenZeppelin StandardToken(相容 ERC20 並加上額外安全防護,是 OpenZeppelin 一系列含募資上限、拍賣、歸屬時程等複雜代幣的基礎)。書自己的 METoken 範例就是 contract METoken is StandardToken,整份合約只有幾行,功能全部繼承自 OpenZeppelin。

7. 書對 ERC20 的評價:已知問題(“Issues with ERC20 Tokens”)

書的態度是「採用爆炸性成長,但有一些潛在的坑」。以下全部是書中實際寫到的

(1) 把代幣送到不支援代幣的合約地址=永久卡死。 書故意示範了這個災難:把 1,000 MET 送進前面章節的 Faucet 合約,然後問「怎麼領回來?」答案是:

“If you’re wondering what to do next, don’t. There is no solution to this problem. The MET sent to Faucet is stuck, forever. Only the Faucet contract can transfer it, and the Faucet contract doesn’t have code to call the transfer function of an ERC20 token contract.”
(譯文:如果你正在想接下來該怎麼辦——別想了。這個問題沒有解。送進 Faucet 的那些 MET 永遠卡住了。只有 Faucet 合約能轉走它們,而 Faucet 合約裡並沒有呼叫 ERC20 代幣合約 transfer 函式的程式碼。)
—— Ch.10 “Sending ERC20 tokens to contract addresses”(PDF p.451)

規模:書引述「據某些估計,寫作當時價值超過約 250 萬美元的代幣就這樣『卡住』並永遠遺失」。最常見的踩坑方式是使用者想把代幣送到交易所或其他服務——他們從交易所網站複製一個以太坊地址,以為直接送過去就好,但許多交易所公布的收款地址其實是合約,那些合約只設計來收 ether(通常會把收到的資金掃進冷錢包或另一個中心化錢包)。儘管到處寫著「請勿發送代幣到此地址」的警告,還是有大量代幣這樣消失。

(2) 代幣移轉根本沒有交易送到收款人。 這是書認為「比較不明顯」的問題:

“In a token transfer, no transaction is actually sent to the recipient of the token. Instead, the recipient’s address is added to a map within the token contract itself. A transaction sending ether to an address changes the state of an address. A transaction transferring a token to an address only changes the state of the token contract, not the state of the recipient address.”
(譯文:在一次代幣移轉中,實際上沒有任何交易被送到代幣的收款人。取而代之的是,收款人的地址被加進代幣合約內部的一張對映表裡。送 ether 到某個地址的交易會改變該地址的狀態;而把代幣轉到某個地址的交易,只會改變代幣合約的狀態,不會改變收款地址的狀態。)
—— Ch.10 “Issues with ERC20 Tokens”(PDF p.455)

後果:即使錢包支援 ERC20,除非使用者明確把某個代幣合約加進「watch」清單,否則錢包不會知道有這個餘額。有些錢包會盯著最熱門的代幣合約,但那只涵蓋現存 ERC20 合約的極小部分。

(3) 垃圾代幣(junk token)。 反過來說,使用者也不會想追蹤所有可能的 ERC20 合約——「許多 ERC20 代幣比較像 email 垃圾信而不是可用的代幣」,它們會自動替有 ether 活動的帳戶建立餘額來吸引使用者。歷史悠久的地址(尤其是預售時期建立的)會「裝滿」憑空冒出來的垃圾代幣;當然地址本身沒有真的裝著代幣,是那些代幣合約裡有你的地址。

(4) 送代幣需要 ether,收代幣不需要——幻覺就這樣破了。

“To send ether or use any Ethereum contract you need ether to pay for gas. To send tokens, you also need ether. You cannot pay for a transaction’s gas with a token and the token contract can’t pay for the gas for you.”
(譯文:要發送 ether 或使用任何以太坊合約,你都需要 ether 來支付 gas。要發送代幣,你同樣需要 ether。你無法用代幣支付一筆交易的 gas,代幣合約也不能替你支付 gas。)
—— Ch.10 “Issues with ERC20 Tokens”(PDF p.456)

書描述的使用者經驗:你在交易所或 ShapeShift 把比特幣換成某代幣,錢包顯示餘額、看起來跟其他加密貨幣沒兩樣;一按送出,錢包告訴你需要 ether。你可能根本不知道它是以太坊上的 ERC20,還以為它有自己的區塊鏈。「幻覺就這樣破了。」

(5) 代幣的行為跟 ether 不一樣。 ether 用 send 發送,可被合約中任何 payable function 或任何 EOA 接受;代幣則透過只存在於 ERC20 合約內的 transferapprove + transferFrom 發送,且(至少在 ERC20 中)不會觸發收款合約裡的任何 payable function

(6) 責任被推給使用者介面。 書在 METFaucet 範例做完後直接下結論:只要正確使用,ERC20 代幣可以被 EOA 與其他合約使用,「然而,正確管理 ERC20 代幣的負擔被推給了使用者介面」——如果使用者誤把代幣轉到一個沒有能力接收 ERC20 的合約地址,代幣就沒了。

書自己對這批問題的分類:有些是 ERC20 特有的;有些是以太坊內部抽象與介面邊界的普遍問題(EOA 與合約的區別、交易與訊息的區別);有些可以靠改代幣介面解決,有些需要改以太坊的基礎結構,有些可能根本「無解」,只能靠 UI 設計把細節藏起來。

8. 兩個提案中的改良標準

書把它們放在「proposed」層級,並明說爭論仍在繼續。

ERC223(p.457–458):靠偵測目的地是不是合約來解決誤轉問題。它要求「設計來接受代幣的合約必須實作一個名為 tokenFallback 的 function」;如果轉帳目的地是合約而該合約沒有實作 tokenFallback,轉帳就失敗。偵測手法是一小段 inline assembly:

function isContract(address _addr) private view returns (bool is_contract) {
  uint length;
    assembly {
       // retrieve the size of the code on target address; this needs assembly
       length := extcodesize(_addr)
    }
    return (length>0);
}

書的評價:ERC223 並未被廣泛實作,ERC 討論串中對於「向後相容性」以及「該在合約介面層還是使用者介面層做改動」的取捨仍有爭論。

ERC777(p.459–461):目標包含——提供 ERC20 相容介面;用 send function 移轉代幣(類似 ether 的轉帳);相容 ERC820 的代幣合約註冊;透過發送前呼叫的 tokensToSend 讓合約與地址控制自己送出哪些代幣;透過在收款方呼叫 tokensReceived 讓合約與地址得知代幣到帳,並藉由「要求合約必須提供 tokensReceived」來降低代幣被鎖死在合約裡的機率;允許既有合約用 proxy 合約來實作這兩個 hook;不論送給合約或 EOA 行為一致;為鑄造(minting)與銷毀(burning)提供專屬事件;讓 operator(受信任第三方,設想為經過驗證的合約)代表持有者移轉代幣;在 userDataoperatorData 欄位提供轉帳的中繼資料。

限制與爭議:每個地址只能註冊一個 token sender 與一個 token recipient hook(可用 message sender=代幣合約地址來分辨是哪種代幣);收款合約若沒註冊實作該介面的地址,代幣轉帳就會失敗。ERC777 依賴平行提案 ERC820 的註冊合約,因此部分爭論集中在「一次採用兩個大改動(新的代幣標準+註冊標準)的複雜度」。

9. ERC721:非同質化代幣(deed)與 ERC20 的差別

書用的詞是 deed(地契/權狀),並引牛津字典的定義:「一份經簽署與交付的法律文件,特別是關於財產所有權或法定權利者」。書坦承這些東西在任何司法管轄區都還不被承認為「法律文件」——「但很可能在未來的某個時點,基於區塊鏈平台上數位簽章的合法所有權會被法律認可」。

ERC721 追蹤一個獨特事物的所有權:可以是遊戲道具、數位收藏品,也可以是所有權由代幣追蹤的實體物(房子、車子、藝術品);deed 甚至可以代表負值的東西,例如貸款(債務)、留置權、地役權。標準對「被追蹤所有權的那個東西是什麼」不設限制也不設期待,只要求它能被唯一識別,而在此標準中是用一個 256-bit 的識別碼達成

跟 ERC20 的差別,書說只要看一行資料結構就夠了(p.463):

// Mapping from deed ID to owner
mapping (uint256 => address) private deedOwner;

“Whereas ERC20 tracks the balances that belong to each owner, with the owner being the primary key of the mapping, ERC721 tracks each deed ID and who owns it, with the deed ID being the primary key of the mapping. From this basic difference flow all the properties of a non-fungible token.”
(譯文:ERC20 追蹤的是屬於每個擁有者的餘額,對映表的主鍵是擁有者;而 ERC721 追蹤的是每一個 deed ID 以及誰擁有它,對映表的主鍵是 deed ID。非同質化代幣的所有性質,都由這個基本差異衍生而來。)
—— Ch.10 “ERC721: Non-fungible Token (Deed) Standard”(PDF p.463)

另外書明說:ERC20 只追蹤每個帳戶的最終餘額,並不(明確地)追蹤任何代幣的來源歷史(provenance)——這正好呼應第 2 節「可追蹤 provenance 就不完全同質」的說法。

書中列出的 ERC721 介面原文(p.463):

interface ERC721 /* is ERC165 */ {
    event Transfer(address indexed _from, address indexed _to, uint256 _deedId);
    event Approval(address indexed _owner, address indexed _approved,
                   uint256 _deedId);
    event ApprovalForAll(address indexed _owner, address indexed _operator,
                         bool _approved);
 
    function balanceOf(address _owner) external view returns (uint256 _balance);
    function ownerOf(uint256 _deedId) external view returns (address _owner);
    function transfer(address _to, uint256 _deedId) external payable;
    function transferFrom(address _from, address _to, uint256 _deedId)
        external payable;
    function approve(address _approved, uint256 _deedId) external payable;
    function setApprovalForAll(address _operateor, boolean _approved) payable;
    function supportsInterface(bytes4 interfaceID) external view returns (bool);
}

對照 ERC20 可以看出:同名的 balanceOftransfertransferFromapprove 都在,但參數從「數量」換成了「deed ID」;並多出 ownerOf(問某個 deed 屬於誰)與 setApprovalForAll(一次授權 operator 管理全部)。

ERC721 另外支援兩個選用介面metadatanamesymboldeedUri)與 enumerationtotalSupplydeedByIndexcountOfOwnersownerByIndexdeedOfOwnerByIndex)。

10. 該不該用標準、用哪一份實作、要不要擴充

標準是什麼(“What Are Token Standards? What Is Their Purpose?”,p.466):標準是實作的最低規格——要符合 ERC20,你至少要實作它指定的 function 與行為;你也可以自由加上標準之外的功能。

“The primary purpose of these standards is to encourage interoperability between contracts.”
(譯文:這些標準的主要目的,是促進合約之間的互通性。)
—— Ch.10 “What Are Token Standards? What Is Their Purpose?”(PDF p.466)

具體好處:所有錢包、交易所、使用者介面與其他基礎設施元件,都能以可預期的方式與任何遵循該規格的合約互動——你部署一份符合 ERC20 的合約,所有既有錢包的使用者就能無縫開始交易你的代幣,不需要升級錢包,你也不必額外做什麼。另一個關鍵性質:標準是描述性的(descriptive)而非規定性的(prescriptive),怎麼實作那些 function 由你決定,合約內部運作與標準無關;標準只有少數規範特定情況下行為的功能性要求(書舉的例子:transfer 在 value 為零時的行為)。

該不該用(p.467):這是兩難。標準必然限制你創新的能力,替你挖了一條必須遵循的窄溝;但基本標準是從數百個應用的經驗中浮現的,通常很貼合絕大多數使用情境。更大的議題是互通性與廣泛採用的價值——選既有標準,你就獲得所有為該標準設計的系統帶來的價值;選擇偏離,你得考慮自己從頭打造全部支援基礎設施、或說服別人支援你的新標準的成本。書點名這種「凡事自己來、無視既有標準」的傾向叫 “Not Invented Here” 症候群,與開源文化背道而馳;但也補上一句:進步與創新有時就是得偏離傳統。

Security by maturity(p.468):選完標準還要選實作,而這個選擇有嚴重的安全意涵。

“Existing implementations are “battle-tested.” While it is impossible to prove that they are secure, many of them underpin millions of dollars’ worth of tokens. They have been attacked, repeatedly and vigorously. So far, no significant vulnerabilities have been discovered.”
(譯文:既有實作是「經過實戰檢驗」的。雖然無法證明它們安全,但其中許多支撐著價值數百萬美元的代幣,並且反覆而猛烈地被攻擊過。到目前為止,尚未發現重大漏洞。)
—— Ch.10 “Security by Maturity”(PDF p.468)

書的建議:自己寫不容易,合約被攻破的微妙途徑很多,用經過充分測試、被廣泛使用的實作安全得多;書的範例用的就是 OpenZeppelin 的 ERC20 實作,因為它從根基上就以安全為導向。要擴充也要小心——

“Complexity is the enemy of security. Every single line of code you add expands the attack surface of your contract and could represent a vulnerability lying in wait.”
(譯文:複雜性是安全的敵人。你加的每一行程式碼都在擴大合約的攻擊面,都可能是一個潛伏等待的漏洞。)
—— Ch.10 “Security by Maturity”(PDF p.468)

常見的擴充功能(“Extensions to Token Interface Standards”,p.469–470):Owner control(給特定地址或多簽群組特殊能力:黑名單、白名單、鑄造、回收等)、Burning(把代幣送到不可花用的地址或直接抹掉餘額並減少供給)、Minting(以可預期的速率或依創建者「意志」增加總供給)、Crowdfunding(透過拍賣、市場銷售、反向拍賣等出售代幣)、Caps(為總供給設定預先定義且不可變的上限,與 minting 相反)、Recovery backdoors(由指定地址啟動的資金回收、反向轉帳或拆除代幣的函式)、Whitelisting(把轉帳等動作限制在特定地址,最常見於各法域審核後只提供給「合格投資人」)、Blacklisting(禁止特定地址轉帳)。書提醒:這些功能目前沒有被廣泛接受的介面標準,而是否擴充標準本身,就是「創新/風險」與「互通性/安全」之間的取捨。

11. DApp 架構與「什麼該上鏈」

這部分見 工具-去中心化儲存與命名(同書 Ch.12):DApp 的定義與五個可去中心化的面向(後端邏輯、前端、資料儲存、訊息通訊、名稱解析)、大檔案為什麼不能上鏈(IPFS/Swarm)、使用者怎麼不用記 0x 地址(ENS)、以及要不要留特權帳號的治理取捨,全部在那張卡。

本卡只留一句銜接:代幣合約是 DApp「鏈上那一半」的典型形態——Ch.10 結尾也預告,Ch.12 會用一個非同質化代幣當作拍賣 DApp 的基礎。

🧪 我實際套用的紀錄

  • 2026-07-15:(待填)

⚠️ 注意 / 什麼時候不適用

  • 書對當時代幣市場的評價非常不客氣(“Tokens and ICOs”,p.471):代幣標準與平台長期影響可能巨大,但不該和對當前代幣發行的背書混為一談——「如同任何早期技術,第一波產品與公司幾乎都會失敗,有些會敗得很慘。今天以太坊上提供的許多代幣,不過是勉強偽裝的騙局、金字塔騙局與撈錢手法。」書在 ICO 的 NOTE 也特別聲明:書中對 ICO 的說明與範例不構成對這種募資方式的背書。
  • 這是 2018 年的快照。 書自己把 ERC223、ERC777、ERC820 都標為「proposed/討論仍在繼續」,ERC721 引用的也是當時的提案版本(介面裡是 transferdeedUricountOfOwners,用詞是 deed)。要動手實作前必須查現行規格,不要照書抄介面。
  • 標準只是最低規格,符合 ERC20 不代表安全;安全的整體設計見 工具-智能合約安全反模式(書自己在 “Security by Maturity” 的 TIP 就指向 Chapter 9)。
  • 代幣一定要有 ether 才轉得動,這個限制連帶決定了很多產品的入金流程設計;gas 那一層見 工具-Gas與EVM
  • decimals 是純顯示約定:書的 METoken 用 decimals = 2,所以「轉 1,000 MET」在合約裡要寫 100000。這種單位換算錯誤在介面層很容易發生。

刻意不寫的東西

本書出版於 2018 年,以下都不在本章內,本卡一律不寫

  • approve 的競態(race condition)/先跑(front-running)問題——這是 ERC20 有名的坑,但本章完全沒有提到racefront-run 在 Ch.10 全章 grep 為 0 命中)。書列出的 ERC20 問題只有上面第 7 節那六項。不代書補。(Ch.9 有講交易排序/搶跑,但那是合約安全的一般議題,見 工具-智能合約安全反模式,不是本章對 ERC20 的評價。)
  • ERC-1155(多代幣標準)——本章沒有,全章 grep 0 命中。原卡片提到的 ERC-1155 已移除。
  • ERC-4626(金庫標準)、現代 NFT 生態(NFT 市集、版稅、鏈下 metadata 慣例、tokenURIsafeTransferFrom 這些最終定案的 ERC-721 命名)——書中一律沒有。
  • DeFi、AMM、流動性池、穩定幣機制——本章談代幣的用途時只列到第 1 節那 10 種,沒有 DeFi 這個概念。
  • EIP-1559 之後的 gas 機制——本章談到代幣轉帳需要 gas 時,用的是「你需要 ether 來付 gas、代幣不能付 gas」的說法,沒有 base fee/priority fee。相關的 gas 卡也維持書中的單一 gasPrice 模型。

🔗 相關工具

  • 工具-去中心化儲存與命名 —— 同書 Ch.12:DApp 的定義、前端/儲存/命名怎麼去中心化(本卡的第 11 節直接指向那裡)。
  • 工具-智能合約 —— 代幣=一份智能合約;那張講合約本身的性質與不可變性。
  • 工具-智能合約安全反模式 —— 同書 Ch.9:“Security by Maturity” 明白指向那一章;代幣合約管的是真實價值,權限與升級設計的坑都在那裡。
  • 工具-Gas與EVM —— 同書 Ch.13:為什麼送代幣一定要有 ether,以及 transfer 那筆交易的成本從哪來。
  • 工具-以太坊帳戶與交易 —— 同書 Ch.6:代幣移轉「只改代幣合約狀態、不改收款地址狀態」,要先懂交易與帳戶模型才看得懂這句。
  • 工具-針對介面編程 —— 標準介面=針對介面編程;書自己的說法是「標準是描述性而非規定性的」。
  • 精通以太坊 —— 來源書卡

🎯 什麼情境該想到我

當你的 DApp 已經有智能合約(鏈上那一半),卻卡在這些問題時:

  • 大檔案放不進鏈上。書中 Ch.12 “Data Storage”(PDF p.501)直說:

    “Due to high gas costs and the currently low block gas limit, smart contracts are not well suited to storing or processing large amounts of data.”
    (譯文:由於 gas 成本高昂、且目前區塊 gas 上限偏低,智能合約並不適合儲存或處理大量資料。)

  • 使用者記不住 0x 開頭的地址。Ch.12 “The Ethereum Name Service (ENS)“(p.518)舉的例子:以太坊基金會捐款地址是 0xfB6916095ca1df60bB79Ce92cE3Ea74c37c5d359,在支援 ENS 的錢包裡就只是 ethereum.eth
  • 前端還掛在自己的 web server 上,等於整個「去中心化應用」有一顆單點故障的心臟。
  • 要決定 DApp 該不該留管理員鑰匙(“DApp governance”,p.508–509)。

先記住本章對 DApp 的定義與它主張的三個優點(“What Is a DApp?”,p.497–498):

“A DApp is an application that is mostly or entirely decentralized.”
(譯文:DApp 是一個大部分或完全去中心化的應用程式。)

可被去中心化的五個面向:後端邏輯、前端、資料儲存、訊息通訊、名稱解析。三個優點小節分別是:

優點書中說法(摘要)
Resiliency(韌性)業務邏輯由智能合約掌控,後端完全分散在區塊鏈上;只要平台還在運作,DApp 就沒有停機時間
Transparency(透明)鏈上性質讓任何人都能檢視程式碼、確認其功能;所有互動永久留存於區塊鏈
Censorship resistance(抗審查)只要使用者能存取一個以太坊節點(必要時自己跑一個),就能不受任何中心化控制干擾地與 DApp 互動;部署後連合約擁有者本人都無法更改程式碼

但書也提醒,寫作當下(2018)「真正」的 DApp 極少,多數仍在某些環節依賴中心化服務與伺服器。

⚙️ 怎麼用

A. 我的 DApp 要放圖片/大檔案怎麼辦

1. 認清鏈上不是檔案系統。 “Data Storage”(p.501):

“Decentralized P2P storage is ideal for storing and distributing large static assets such as images, videos, and the resources of the application’s frontend web interface (HTML, CSS, JavaScript, etc.).”
(譯文:去中心化的 P2P 儲存最適合用來儲存與散布大型靜態資產,例如圖片、影片,以及應用程式前端網頁介面的各項資源(HTML、CSS、JavaScript 等)。)

2. 選一個內容定址(content-addressable)的 P2P 儲存。 書中給兩個選項:

  • IPFS(“IPFS”,p.501):

    “‘Content addressable’ means that each piece of content (file) is hashed and the hash is used to identify that file.”
    (譯文:「內容定址」的意思是:每一份內容(檔案)都會被雜湊,而這個雜湊值就用來識別該檔案。)
    IPFS 的目標是取代 HTTP 成為傳遞網頁應用的協定——網頁應用不再放在單一伺服器上,而是存在 IPFS 上、可從任一 IPFS 節點取回。

  • Swarm(“Swarm”,p.501–502):

    “Swarm is another content-addressable P2P storage system, similar to IPFS. Swarm was created by the Ethereum Foundation, as part of the Go-Ethereum suite of tools.”
    (譯文:Swarm 是另一套內容定址的 P2P 儲存系統,和 IPFS 類似。Swarm 由以太坊基金會打造,屬於 Go-Ethereum 工具組的一部分。)
    Swarm 自己的首頁就存在 Swarm 上(https://swarm-gateways.net/bzz:/theswarm.eth/),本身就是示範。

3. 實作步驟(書中 “Preparing Swarm” 起,p.514–517):

  1. 安裝 Swarm(Go-Ethereum 工具組的一部分),swarm version 確認可用(書中示範版本為 0.3)。
  2. 啟動 Swarm 時要告訴它怎麼連上一個 Geth 實例以存取 JSON-RPC API;啟動記錄裡會看到 connecting to ENS API url=http://127.0.0.1:8545
  3. http://localhost:8500 的本機 Swarm gateway 網頁介面,確認節點正常,並可查詢任何 Swarm hash 或 ENS 名稱。
  4. 單檔上傳:swarm up code/auction_dapp/README.md → 回傳一個 hash,之後任何 Swarm 節點都能靠這個 hash 取檔。
  5. 整個前端要先打包。因為 HTML/CSS/JS/函式庫彼此有內嵌參照,平常是 web server 幫忙把 URL 轉成本地檔案;上 Swarm 前要 npm run build 打包成 dist/
  6. 遞迴上傳並指定入口:
    swarm --bzzapi http://localhost:8500 --recursive \
      --defaultpath dist/index.html up dist/
    
    得到一個 hash,整個 DApp 就能用 bzz://<hash> 存取。

書中還提到第三個面向:訊息通訊可用 Whisper(同屬 Go-Ethereum 工具組),為每場拍賣開一間不需中央伺服器的聊天室。

B. 怎麼讓使用者不用記 0x 開頭的地址

上傳完 Swarm 後書自己吐槽:bzz://ab164cf37dc... 這種 URL 遠不如 auction_dapp.com 好記。解法是 ENS

1. 先理解 ENS 的分層(“sandwich” 三明治設計): 底層極簡、中層複雜但可替換、頂層極簡且把資金放在各自獨立的帳戶裡。規格散在三份 EIP:EIP-137(ENS 基本功能)、EIP-162.eth root 的拍賣系統)、EIP-181(地址的反向解析)。

2. Namehash:名字先變成 node。(“Bottom Layer” / “The Namehash algorithm”,p.521–522)ENS 底層操作的是「node」而非人類可讀的名字,兩者靠遞迴的 Namehash 演算法轉換:

namehash([]) = 0x0000000000000000000000000000000000000000000000000000000000000000
namehash([label, ...]) = keccak256(namehash(...) + keccak256(label))

遞迴的 base case 就是 root node:

“The root node is defined as 0x0000000000000000000000000000000000000000000000000000000000000000 (32 zero bytes).”
(譯文:root node 被定義為 0x000…000(32 個零位元組)。)

所以 subdomain.example.eth 的 node 是 keccak(keccak(keccak(0x0...0 + keccak('eth')) + keccak('example')) + keccak('subdomain'))。實務上的好處:Namehash 只取決於名字本身,可以預先算好塞進合約,省掉字串處理,不論名稱有幾層都能立即查表。

3. 取名要合法(“How to choose a valid name”,p.522–523)。大小寫都允許,但所有 label 應先做 UTS #46 正規化(case-fold 後才雜湊),所以拼字相同、大小寫不同的名字會得到同一個 Namehash。為了和既有 DNS 相容,書建議:

  • 每個 label 不超過 64 字元
  • 完整 ENS 名稱不超過 255 字元
  • label 不可以連字號開頭或結尾,也不可以數字開頭

4. 註冊:.eth 走改良版 Vickrey 拍賣(“Vickrey auctions”,p.525–526)。傳統次價密封拍賣的性質:

“In a traditional Vickrey auction, every bidder submits a sealed bid, and all of them are revealed simultaneously, at which point the highest bidder wins the auction but only pays the second-highest bid.”
(譯文:在傳統的 Vickrey 拍賣中,每位競標者提交一份密封標,所有標同時揭曉,此時出價最高者贏得拍賣,但只需支付第二高的出價。)

為什麼選它:因為出價者被誘導不要低於名字對自己的真實價值去出價——照真實價值出價會提高中標機率,卻不影響最終要付的金額。

放到區塊鏈上必須改三件事:(a) 出價者必須事先鎖入等於或高於其出價的金額,確保不會亂喊不付;(b)

“Because you can’t hide secrets on a blockchain, bidders must execute at least two transactions (a commit–reveal process), in order to hide the original value and name they bid on.”
(譯文:因為在區塊鏈上藏不住秘密,出價者必須執行至少兩筆交易(一個 commit–reveal 流程),才能隱藏他們原本的出價金額與所競標的名稱。)

(c) 去中心化系統無法「同時」揭曉所有標,只能由出價者自己揭標;不揭標就沒收鎖定的資金——否則有人會廣撒標單、只挑一兩個揭曉,把密封標拍賣變成傳統的加價拍賣。

因此拍賣是四步:開標(名稱經雜湊,只有字典裡有該名字的人才知道開了哪場;還可同時開多個假標混淆跟蹤者)→ 密封出價(把一筆 ether 綁到一段秘密訊息的雜湊上,內含名稱雜湊、實際出價與 salt;可以鎖超過實際出價來掩飾真實估值)→ 揭標(每次揭標都重算當前贏家,揭標期限前最後被設定的那個成為最終贏家;未中標者退款)→ 收尾(贏家 finalize 拿回出價與次高價的差額;忘記揭標可補晚揭回收一小部分)。

書中實作示範是用 MyCrypto 介面搭 MetaMask 標下 ethereumbook.eth,並附了血淚警告:出價後 48 小時內必須揭標,否則資金沒收——作者自己就這樣賠掉 0.01 ETH。

5. 頂層 The Deeds:資金怎麼放。 得標後資金不是付給誰,而是鎖定在你持有名字的期間(至少一年),像有保證的買回機制:不想要名字時賣回系統就能取回 ether,持有成本等於這筆錢的機會成本。為避免單一合約握有數百萬美元的風險,ENS 為每個新名字建立一份約 50 行的 deed 合約,只允許把資金退回單一帳戶(deed 擁有者)、只能被單一實體(registrar 合約)呼叫,大幅縮小攻擊面。

6. Resolver:名字要指向什麼,由解析器決定(“Resolvers”,p.523–524):

“The basic ENS contract can’t add metadata to names; that is the job of so-called ‘resolver contracts.’”
(譯文:基本的 ENS 合約無法為名稱附加中繼資料,那是所謂「解析器合約」的工作。)

解析是兩步:先把名稱雜湊後呼叫 ENS registry,若記錄存在就回傳其 resolver 的地址;再呼叫該 resolver,用對應資源的方法取得結果。這種分離讓你可以自訂 resolver 回答任何型別的資源(書舉例:未來想把經緯度地理位置綁到 ENS 名稱,寫個新 resolver 即可)。一般情況用預設的 public resolver 就好,它支援 address(錢包或合約)與 content(DApp 的 Swarm hash 或合約原始碼)。

7. 串起來:subdomain + content。ENS Manager 建子網域(書中示範 auction.ethereumbook.eth),設好 public resolver,再把 content 設成前端的 Swarm hash。結果從:

bzz://ab164cf37dc10647e43a233486cdeffa8334b026e32a480dd9cbd020c12d4581

變成:

http://swarm-gateways.net/bzz:/auction.ethereumbook.eth/

也能直接在任何支援 ENS 的錢包或 DApp 瀏覽器裡搜尋這個名字。

C. 決定治理:要不要留特權帳號

“DApp governance”(p.508–509)把這件事講成雙面刃:有特權帳號很危險,一旦被攻破就能顛覆整個 DApp 的安全性;完全沒有特權帳號,則在發現 bug 時毫無補救手段。書舉兩個反面案例:The DAO 的「curators」權限太有限,擋不住攻擊者提走資金;去中心化交易所 Bancor 則因為一個特權管理帳號被攻破而遭鉅額竊盜——「結果證明 Bancor 並沒有它最初被認為的那麼去中心化」。書的結論是:

“Either choice carries risk, but in the long run, true DApps cannot have specialized access for privileged accounts — that’s not decentralized.”
(譯文:兩種選擇都有風險,但長遠來看,真正的 DApp 不能為特權帳號保留特殊存取權——那不叫去中心化。)

🧪 我實際套用的紀錄

  • (待填)

⚠️ 注意 / 什麼時候不適用

  • 本卡完全依據《Mastering Ethereum》(O’Reilly, 2018) Ch.12 原文。 ENS 的 .eth 註冊機制在本書之後已經改版,書中描述的 Vickrey 密封拍賣+deed 鎖定資金是 2017–2018 年當時的做法,實際要註冊名字前必須查現行規格,不要照書操作。書中自己也標明「at the time of writing」。
  • root node 的治理是有名有姓的中心化風險(“Root node ownership”,p.523):整個階層系統依賴 root node 擁有者來建立頂級域名(TLD),而「寫作當下 root node 由一個 4-of-7 多重簽章控制」,由分散在不同國家的人持有(比照 DNS 的 7 位金鑰持有者設計),任何變更都需要 7 人中至少 4 人同意。書列出的目標包含把 .eth TLD 遷移到更永久的合約、必要時新增 TLD、把 root multisig 遷到更去中心化的合約,以及在頂層註冊系統出現漏洞時作為最後手段。長期目標是去中心化,但當下不是。
  • .eth 幾乎是唯一選項(“Middle Layer: The .eth Nodes”,p.525):寫作當下唯一能在智能合約中唯一註冊的 TLD 就是 .eth。讓傳統 DNS 域名持有者認領 ENS 所有權的工作仍在進行中,理論上 .com 可行,但目前只對 .xyz 實作,而且只在 Ropsten 測試網
  • 拍賣合約是複雜度熱區:書明說 .eth 拍賣系統超過 500 行,ENS 早期的開發心力(與 bug!)多半集中在這一塊;好在它可替換、可升級且不危及資金。
  • 48 小時揭標期限會吃掉你的錢,這是流程性的操作風險,不是技術問題。
  • 鏈下運算=信任外部資源:書在 “Backend (Smart Contract)“(p.499)提醒,若把核心業務邏輯依賴外部資料(例如中心化伺服器),使用者就必須信任這些外部資源。搬到鏈下省 gas 的同時,信任模型也一起改變了。
  • 不要把「有網頁前端的智能合約」當成 DApp:本章開頭的 WARNING 直接造了個詞——這類高度中心化的應用該叫 CApp,「小心假的 DApp!」

🔗 相關工具

🔮 連接外部世界

🎯 什麼情境該想到我

當你的合約需要「鏈上本來就沒有的東西」時——外部資料、真正的亂數、或是鏈上算不完的運算。

書中對預言機的定義(Ch.11 章首,p.473):

In the context of blockchains, an oracle is a system that can answer questions that are external to Ethereum. Ideally oracles are systems that are trustless, meaning that they do not need to be trusted because they operate on decentralized principles.

(譯文)在區塊鏈的脈絡下,預言機是一種能回答「以太坊之外的問題」的系統。理想上預言機應該是無信任的(trustless),意思是它們不需要被信任,因為它們基於去中心化的原則運作。

名字來自希臘神話(Ch.11 章首,p.473):

The term “oracle” comes from Greek mythology, where it referred to a person in communication with the gods who could see visions of the future.

(譯文)「oracle」一詞源自希臘神話,指的是能與神溝通、能看見未來景象的人。


⚙️ 怎麼用

1. 先搞懂為什麼非要預言機不可(這是整章的靈魂)

Ch.11 “Why Oracles Are Needed”(p.474)的論證:共識要求 EVM 完全確定性,而這一個要求直接推導出兩個後果——

In order to maintain consensus, EVM execution must be totally deterministic and based only on the shared context of the Ethereum state and signed transactions. This has two particularly important consequences: the first is that there can be no intrinsic source of randomness for the EVM and smart contracts to work with; the second is that extrinsic data can only be introduced as the data payload of a transaction.

(譯文)為了維持共識,EVM 的執行必須是完全確定性的,且只能基於「以太坊狀態」與「已簽名交易」這個共享脈絡。這帶來兩個特別重要的後果:第一,EVM 與智能合約沒有任何內生的亂數來源可用;第二,外部資料只能以交易的 data payload 形式被引入

為什麼亂數不行:同一份合約在節點 A 執行存下 3、節點 B 執行存下 7,同樣的程式碼、同樣的脈絡卻得到不同狀態,網路永遠無法對結果達成去中心化共識;而且連鎖效應(包括 ether 轉帳)會指數級放大。

為什麼偽隨機也不夠(p.474):雜湊函數是確定性的、可以放進 EVM,但對很多應用不夠。

a miner can gain an advantage by playing the game and only including their transactions in blocks for which they will win.

(譯文)礦工可以透過自己下場玩、且只把「自己會贏」的那些交易打包進區塊,來取得優勢。

為什麼「塞進交易裡」不算解決(p.475):所有節點都能對已簽名交易的內容取得共識,所以亂數、價格、天氣預報都可以當作交易的 data 部分送進網路,但——

However, such data simply cannot be trusted, because it comes from unverifiable sources. As such, we have just deferred the problem.

(譯文)然而這種資料根本不可信,因為它來自無法驗證的來源。所以我們只是把問題往後延而已。

👉 實務判準:問題從來不是「資料進不進得來」,而是「進來的資料憑什麼可信」。 你選預言機時,真正在選的是信任模型。

2. 確認你的需求真的在使用場景清單裡

Ch.11 “Oracle Use Cases and Examples”(p.476–478)列出的完整清單:

  • 實體來源的亂數/熵(量子/熱力學過程):例如樂透合約公平選出贏家
  • 與自然災害連動的參數化觸發器:例如地震債券用芮氏規模觸發巨災債券合約
  • 匯率資料:例如加密貨幣對法幣的精確錨定
  • 資本市場資料:例如為代幣化資產/證券的組合定價
  • 基準參考資料:例如把利率帶進智慧金融衍生品
  • 靜態/半靜態資料:證券識別碼、國碼、幣別代碼等
  • 時間與區間資料:需要精確時間量測的事件觸發
  • 天氣資料:例如依天氣預報計算保費
  • 政治事件:預測市場的結算
  • 體育賽事:預測市場結算與夢幻運動合約
  • 地理位置資料:例如供應鏈追蹤
  • 損害驗證:保險合約
  • 其他區塊鏈上發生的事件:互通性功能
  • 以太幣市價:例如法幣計價的 gas price 預言機
  • 航班統計:例如社團/俱樂部的機票 pooling

書中也提醒另一類:學歷證明、政府 ID 這種來自特定私有資料源的資料,其來源(大學、政府部門)是被完全信任的,真相是主觀的(只能訴諸來源的權威),因此本質上無法 trustless 地提供。這類資料通常以 attestation(護照、成就紀錄)的形式呈現。

3. 知道所有預言機都在做同樣三件事

Ch.11 “Oracle Design Patterns”(p.479):

Collect data from an off-chain source. / Transfer the data on-chain with a signed message. / Make the data available by putting it in a smart contract’s storage.

(譯文)從鏈下來源蒐集資料/用已簽名的訊息把資料送上鏈/把資料放進智能合約的 storage 使其可被取用。

資料一旦進到 storage,其他合約可以用 message call 呼叫預言機合約的 “retrieve” 函式取用,節點或連網用戶端也可以直接「窺看」storage。

4. 選對三種模式之一

The three main ways to set up an oracle can be categorized as request–response, publish-subscribe, and immediate-read.

(譯文)設置預言機的三種主要方式可分為:請求–回應、發布–訂閱、即時讀取。

模式適用情境關鍵取捨
immediate-read(即時讀取)只為「當下的決定」而需要的資料,just-in-time 查一次、可能之後再也不查。例:「ethereumbook.info 的位址是什麼?」「這個人滿 18 歲了嗎?」學歷證明、機構會員、機場代碼、自主身分 ID資料在合約 storage 存一次(可更新);連網應用可直接查詢,不必發交易、不用付 gas。對「本來要自己養伺服器回答查詢」的組織特別划算
publish–subscribe(發布–訂閱)資料會(頻繁且規律地)變動,需要廣播服務。例:價格 feed、天氣、經濟社會統計、交通資料模式類似 RSS/WebSub;訂閱者要輪詢或監聽更新。在 P2P 環境輪詢不像 web server 那麼糟(Ethereum client 本來就得跟上所有狀態變化,等於本地呼叫),配上 event log 幾乎可視為 push;但若是從合約裡輪詢,gas 會很可觀
request–response(請求–回應)資料空間大到塞不進合約,使用者一次只需要一小部分。也適合資料供應商的商業模式最複雜;鏈上合約+鏈下基礎設施,非同步多步驟

request–response 的七個步驟(p.481–482):

  1. 從 DApp 接收查詢
  2. 解析查詢
  3. 檢查付款與資料存取權限是否齊備
  4. 從鏈下來源取得相關資料(必要時加密)
  5. 把含資料的交易簽名
  6. 把交易廣播到網路
  7. 排程後續必要的交易,例如通知等

流程細節:EOA 與 DApp 互動 → 觸發預言機合約中的函式發出 request(參數含所需資料,另可帶 callback 函式與排程參數)→ 交易驗證後,request 可透過預言機合約發出的 EVM event 或狀態變更被觀察到 → 鏈下實際查詢 → 結果由預言機所有者簽名,證明該資料在該時間點的有效性 → 送回 DApp(直接或經預言機合約)。

選型判準(書中的例子):一張需要利率的 smart bond,在 request–response 下可能得每天請求一次以確保利率永遠正確;但既然利率變動不頻繁,publish–subscribe 更合適——尤其考慮到以太坊有限的頻寬。

補充:也可以不要預言機合約,直接由 EOA 請求/回應;請求對象也可以是 IoT 硬體感測器。

Therefore, oracles can be human, software, or hardware.

(譯文)因此,預言機可以是人、軟體,或硬體。

還有 broadcast/multicast 變體:預言機把所有訊息發到某個 channel,訂閱合約用不同訂閱模式監聽——例如發到「加密貨幣匯率」channel,有的合約要整個時間序列算移動平均,有的只要最新價算即期價格。當預言機不需要知道訂閱者是誰時,用 broadcast。

5. 決定資料怎麼被驗證

Ch.11 “Data Authentication”(p.483)指出:就算資料來源本身權威可信(這假設本身就不小),預言機與 request–response 機制可能由不同實體營運,資料有在傳輸中被竄改的可能。

Two common approaches to data authentication are authenticity proofs and trusted execution environments (TEEs).

(譯文)資料驗證的兩種常見做法是「真實性證明」與「可信執行環境(TEE)」。

  • Authenticity proofs(真實性證明):資料未被竄改的密碼學保證,基於各種見證技術(例如數位簽章證明)。

    they effectively shift the trust from the data carrier to the attestor (i.e., the provider of the attestation)

    (譯文)它們實際上是把信任從「資料的載送者」轉移到「見證者」(即提供見證的一方)。

    合約在鏈上驗證這個證明,就能在動用資料前先確認完整性。書中舉 Oraclize 為例,其主網可用的一種證明是 TLSNotary proof:HTTPS 本身安全但不支援資料簽名,所以靠 TLSNotary(透過 PageSigner)簽章,把 TLS master key 拆給三方——伺服器(預言機)、auditee(Oraclize)、auditor(Oraclize 用一台可驗證自實例化以來未被修改的 AWS VM)。

  • TEETown Crier 用 Intel SGX 確保 HTTPS 查詢的回應可被驗證為真。SGX 提供三件事:完整性(enclave 內的應用受 CPU 保護、不被其他 process 竄改)、機密性(執行時狀態對其他 process 不透明)、見證(產生數位簽章證明「以 build hash 安全識別的應用」確實跑在 enclave 內)。機密性這一項讓 Town Crier 能處理私密資料——查詢可用該實例的公鑰加密。

6. 如果你缺的是「算力」而不是「資料」

Ch.11 “Computation Oracles”(p.485):預言機也可以拿來做任意運算,這在以太坊有區塊 gas 上限、鏈上運算相對昂貴的前提下特別有用。例如用計算預言機跑運算密集的迴歸分析來估算債券合約的收益。

  • Oraclize:把 Dockerfile 打包上傳 IPFS,Oraclize 用 hash 取回、在 AWS 起 container 執行、把 stdout 結果回傳。跑在可稽核的 t2.micro 上,價值不低時可以查驗是否執行了正確的容器。
  • Cryptlet(Microsoft ESC Framework):在加密膠囊內執行,抽象掉 I/O 等基礎設施,附帶 CryptoDelegate 讓進出訊息自動被簽名、驗證、證明;支援分散式交易,讓合約邏輯能以 ACID 方式處理多步驟、多區塊鏈、跨外部系統的交易。
  • TrueBit:可擴展且可驗證的鏈下運算。用 solver 與 verifier 的誘因機制;解答若被挑戰,就在鏈上跑迭代驗證流程(一種 verification game),每一輪遞迴檢查越來越小的運算子集,直到最後一輪挑戰簡單到讓「裁判」——以太坊礦工——能在鏈上做出最終裁決。實例:Doge–Ethereum bridge 用 TrueBit 驗證 Dogecoin 的 Scrypt PoW(記憶體密集、無法在區塊 gas 上限內算完),已能在 Rinkeby 測試網的合約內安全驗證 Dogecoin 交易。

7. 想要「無信任」就得走去中心化

Ch.11 “Decentralized Oracles”(p.488):

While centralized data or computation oracles suffice for many applications, they represent single points of failure in the Ethereum network.

(譯文)雖然中心化的資料或運算預言機對許多應用來說已經夠用,但它們在以太坊網路中代表單點失效

書中列的方案(以下皆為 2018 年書中所述的提案內容):

  • ChainLink 提出的去中心化預言機網路:三個關鍵合約 + 一份鏈下的資料提供者註冊表。
    • reputation contract:追蹤資料提供者的表現,分數用來填充鏈下註冊表
    • order-matching contract:依 reputation 挑選預言機的出價,敲定包含查詢參數與所需預言機數量的 SLA——買方因此不必直接與個別預言機打交道
    • aggregation contract:用 commit–reveal 收集多個預言機的回應,算出最終集體結果,再把結果餵回 reputation contract
  • SchellingCoin protocol:多方回報數值,取中位數作為「正確」答案;回報者須提供押金,押金會朝「越接近中位數」的值重新分配,藉此誘導大家回報與他人相近的值。這個共同值(Schelling point)預期會接近真實值。
  • Jason Teutsch(TrueBit) 提出的鏈下資料可得性預言機:用一條專屬的 PoW 鏈,正確回報已註冊的資料在某個 epoch 內是否可得;礦工試圖下載、儲存、傳播所有目前註冊的資料,以此保證資料在本地可得。代價是每個礦工都得存並傳播全部資料,但註冊期滿後資料可釋放、儲存空間可重複使用。

8. 客戶端怎麼接(Solidity)

Ch.11 “Oracle Client Interfaces in Solidity”(p.490–493):

  • Oraclize(範例 11-1,ETH/USD 價格 ticker):合約要繼承 usingOraclize;用 oraclize_query 發請求,至少兩個參數——資料源類型URLWolframAlphaIPFScomputation)與該資料源的參數(可用 JSON/XML 解析輔助)。需要付一小筆 ether:涵蓋處理結果、傳送到 __callback 的 gas,外加服務附加費;金額取決於資料源與所需的 authenticity proof 類型。回呼由 Oraclize 控制的帳戶執行,帶回值與 queryId(可用來追蹤多筆 pending callback)。範例中在 __callback 裡再次呼叫 queryTicker() 以持續輪詢。
  • BlockOne IQ(Thomson Reuters,範例 11-2):供私有/許可制網路上的合約請求市場與參考資料。用 initRequest 指定查詢類型與成功/失敗兩個 callback,取得 uint256 id,再用 addArgumentToRequestStringaddArgumentToRequestUint 加參數(RIC 代碼、時間戳),最後 executeRequest。成功可取回 open/high/low/close(OHLC)與 bid/ask。

🧪 我實際套用的紀錄

  • (待填)

⚠️ 注意 / 什麼時候不適用

1. 信任模型是唯一真正重要的問題(Ch.11 “Conclusions”,p.494):

Generally, when considering the use of an oracle be very careful about the trust model. If you assume the oracle can be trusted, you may be undermining the security of your smart contract by exposing it to potentially false inputs.

(譯文)一般而言,考慮使用預言機時,要非常小心它的信任模型。如果你假設預言機可以被信任,你可能正在讓合約暴露於潛在的假輸入之下,從而削弱自己合約的安全性

書中同時說:「if they are trusted sources and can be compromised, they can result in compromised execution of the smart contracts they feed.」(譯文:如果它們是被信任的來源、而且可能被攻破,就會導致它們所餵養的智能合約執行也被攻破。)

2. 合約管的錢越多,攻擊預言機的誘因越大(p.476 的 smart will 例子):

If the inheritance amount controlled by such a contract is high enough, the incentive to hack the oracle and trigger distribution of assets before the owner dies is very high.

(譯文)如果這種合約控制的遺產金額夠大,那麼「駭掉預言機、在所有者死亡前就觸發資產分配」的誘因會非常高。

👉 這使預言機成為合約的信任邊界與攻擊面,評估時請一併參照 工具-智能合約安全反模式

3. 有些資料本質上就不可能 trustless:學歷、政府 ID 這類來源,真相是主觀的(只能訴諸來源權威),沒有可獨立驗證的客觀真相。書中仍把它們算作預言機(因為同樣提供資料橋梁),但不要對它們宣稱去中心化保證

4. 中心化方案有明講的殘留信任假設,別假裝它不存在:

  • Oraclize 的 TLSNotary 路線「does require the assumption that Amazon itself will not tamper with the VM instance」(譯文:確實需要假設 Amazon 自己不會竄改該 VM 實例)
  • Town Crier 的 SGX 路線只在「assuming that we trust Intel/SGX」(譯文:假設我們信任 Intel/SGX)之下成立
  • Oraclize 的運算服務書中直言「this is not a truly decentralized solution」(譯文:這不是一個真正去中心化的解決方案)

5. 去中心化不是免費的——聚合函數本身就是難題(p.488):判定哪個回應無效並不簡單,因為它預設「偏離同儕的離群資料點就是錯的」;用「回應在分布中的位置」算有效性分數,有懲罰正確答案、獎勵平庸答案的風險。ChainLink 因此提供標準聚合合約,同時允許自訂。

6. 模式選錯的代價

  • 用 request–response 拿變動不頻繁的資料(如利率)→ 白白每天發交易
  • 合約內做 publish–subscribe 輪詢(例如無法設計啟動誘因的 DApp)→ 顯著 gas 開銷
  • immediate-read 存原始資料 → 效率與隱私問題。書中的做法:存 hash 而非原文(學歷存證書的 hash;公民 ID 用加鹽的 Merkle tree、只把 root hash 放進 storage)

7. 這張卡是 2018 年的快照:Oraclize、Town Crier、TrueBit、ChainLink 的三合約設計、SchellingCoin,都是《精通以太坊》成書當時的狀態與提案。上面的原理(確定性 → 需要預言機、三種模式、信任模型優先)長期有效;具體服務與實作請另行查證現況。


🔗 相關工具

  • 工具-智能合約 —— 預言機是合約唯一的對外通道;先懂合約不可變 + 管錢的特性,才知道錯誤輸入的代價有多大
  • 工具-智能合約安全反模式 —— 預言機是信任邊界也是攻擊面;「假設預言機可信」本身就是一種可被利用的假設
  • 精通以太坊 —— 來源書(Ch.11 Oracles)