從位元組碼回到原始碼
想像一台自動販賣機。你投入硬幣、按下按鈕,它就吐出一罐飲料——沒有店員、不需信任、無從討價還價。規則被固定在金屬之中,對每個人都一視同仁,凌晨三點和正午跑出來的結果完全一樣。智慧合約就是用程式碼打造的那台販賣機:一支住在區塊鏈固定位址上的程式,邏輯公開、嚴格照寫好的方式執行,部署之後沒有人能伸手進去更改。
上一階你看著 EVM 一個一個地碾過操作碼,也看到 Solidity 如何編譯成原始位元組碼。現在我們反方向操作:你寫 Solidity 這個高階語言,編譯器再把它變成機器真正執行的位元組碼。這篇會打造一個「雖小但仍然真實」的合約——一個計數器——並用它點出每個合約都具備的四個部分。
你第一個合約的解剖
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;
contract Counter {
// state variable: lives permanently in contract storage
uint256 public count;
// constructor: runs exactly once, at deployment time
constructor(uint256 initialCount) {
count = initialCount;
}
// changes state -> must be sent as a transaction -> costs gas
function increment() public {
count += 1;
}
function decrement() public {
require(count > 0, "Counter: cannot go below zero");
count -= 1;
}
// reads state only -> free to call from outside the chain
function get() public view returns (uint256) {
return count;
}
// touches no state at all -> pure
function double(uint256 x) public pure returns (uint256) {
return x * 2;
}
}- 最上方是 SPDX 授權那一行與 `pragma solidity ^0.8.24;` 版本指示。pragma 告訴編譯器你是針對哪個語言版本寫的;^0.8 很關鍵,因為從 0.8.0 起算術溢位會自動回滾,而不再悄悄繞回(wrap)。
- `contract Counter { ... }` 宣告這個合約——一束狀態與程式碼,將被部署到單一位址。可以把它想成一個類別,而它唯一的實例就活在鏈上。
- `uint256 public count;` 是一個狀態變數:合約在每次呼叫之間都會記住的一塊資料。
- `constructor`(建構子)在部署交易期間只執行一次,用來設定初始值——之後它就消失,永遠不會再被執行。
- 那四個 `function`(函式)是合約對外的接觸面:外界被允許送進來的訊息。
狀態變數:合約的永久記憶
`count` 是狀態。當 `increment()` 加上一,那個新值會被寫進合約的 儲存(storage),並永遠留在那裡——跨越每次呼叫、每個區塊、許多年——成為區塊鏈全域世界狀態的一部分。每個全節點都保有一份副本。這正是智慧合約有別於一般程式之處:它的變數能比任何單次執行活得更久。
在底層,每個狀態變數都被排進 32 位元組的 儲存槽。`count` 佔用第 0 槽。讀取一個槽很便宜,但寫入一個槽是 EVM 最昂貴的動作之一——一次冷儲存寫入可能要約 20,000 gas,遠遠輾壓只要個位數 gas 的算術運算。一個好用的心法是:你在鏈上每持久保存一個位元組,都有代價,由送出該交易的人買單。
讀取免費,寫入付 gas:view 與 pure
這是整篇最重要的一個區別。一個會改變狀態的函式——例如 `increment()`——唯有透過被當作交易廣播、被打包進區塊、再由每個節點重新執行,才會生效。那份工作會被計量,你得為它付 gas。標了 `view` 的函式承諾只讀取狀態;標了 `pure` 的則承諾完全不碰狀態(只從它的參數做運算)。
這對成本為何重要?因為 `view`/`pure` 函式什麼都不改,節點可以在本地跑它、直接回傳答案——不需交易、不需區塊、不收費。當你的應用呼叫 `get()`,它送出的是一個讀取請求(一次 `eth_call`),節點免費替你算出結果。但這種免費只限於鏈下讀取:如果一筆交易在內部呼叫某個 `view` 函式,那段執行仍在 EVM 上跑,仍像其他操作碼一樣要付 gas。
# READ — runs locally on a node, no transaction, no gas, costs you nothing $ cast call $ADDR "get()(uint256)" 7 # WRITE — broadcasts a transaction, included in a block, you pay gas $ cast send $ADDR "increment()" --private-key $PK blockHash 0x9f3c... status 1 (success) gasUsed 43124 # now the read reflects the new state $ cast call $ADDR "get()(uint256)" 8
可見性:誰有資格呼叫這個?
每個函式都帶有一個可見性關鍵字,決定誰可以呼叫它。Solidity 有四種,把它們設對是你的第一道防線——一個本該上鎖、卻被留成人人可呼叫的函式,是整個領域最常見(也最昂貴)的漏洞之一。
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;
contract Visibility {
uint256 private secret; // storage, only THIS contract's code names it
// public: callable from outside AND from inside this contract
function publicValue() public view returns (uint256) {
return _bump(); // can call internal helpers
}
// external: callable ONLY from outside (another account or contract)
function setSecret(uint256 v) external {
secret = v;
}
// internal: this contract and any contract that inherits it
function _bump() internal view returns (uint256) {
return secret + 1;
}
// private: only this exact contract, not even children
function _scratch() private pure returns (uint256) {
return 42;
}
}- `public`(公開)——敞開的門。可從鏈外呼叫,也可被合約內其他函式呼叫。狀態變數也可以是 public(就是先前那個自動 getter)。
- `external`(外部)——只能從外部呼叫。你無法在同一合約的另一個函式中直接呼叫 external 函式,除非加上 `this.` 前綴。它是專供使用者與其他合約使用的進入點的自然選擇。
- `internal`(內部)——可供本合約以及任何繼承它的合約使用;對外界不可見。最適合用來放共用的輔助邏輯。
- `private`(私有)——只有這個合約本身能用,連繼承它的子合約都不行。是最緊的範圍。
部署的生命週期,以及接下來
- 寫好 Solidity 原始碼,然後編譯。編譯器會產出兩樣東西:EVM 位元組碼(真正執行的東西)與 ABI(應用程式用來與它對話的呼叫慣例)。
- 送出一筆攜帶該位元組碼的部署交易。EVM 會把你的建構子恰好執行一次、設定初始狀態,並把執行期位元組碼存到一個全新鑄出的合約位址。
- 自此之後,使用者與其他合約透過向該位址送出交易(用於改變狀態,要付 gas)或 eth_call(用於免費讀取)來互動。
你現在已經握有每個合約共有的骨架:存放在 storage 的狀態變數、只跑一次的建構子、由可見性把關的函式,以及把免費讀取與付費寫入分開的 `view`/`pure` 標記。從這裡開始,合約會長出真正的肌肉。下一篇會加入更豐富的資料——型別,而 修飾器緊接著作為護欄登場,再加上用來描述餘額與紀錄的映射與結構——再過幾篇,你就會把這一切組裝成一個可運作的 ERC-20 代幣。