JOVANA
Explore Library Glossary Getting Started Three Levels Fields How it works Mission
Join the mission
All guides

你的第一個真合約:狀態、函式與可見性

別再只是讀位元組碼,開始動手寫 Solidity。打造一個迷你 Counter 合約,弄懂每個合約都離不開的四件事——狀態變數、建構子、函式與可見性——並看清究竟哪些呼叫要付 gas。

從位元組碼回到原始碼

想像一台自動販賣機。你投入硬幣、按下按鈕,它就吐出一罐飲料——沒有店員、不需信任、無從討價還價。規則被固定在金屬之中,對每個人都一視同仁,凌晨三點和正午跑出來的結果完全一樣。智慧合約就是用程式碼打造的那台販賣機:一支住在區塊鏈固定位址上的程式,邏輯公開、嚴格照寫好的方式執行,部署之後沒有人能伸手進去更改。

上一階你看著 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;
    }
}
一份完整、可編譯的 Solidity ^0.8 合約——每個部分都會在下面說明。
  1. 最上方是 SPDX 授權那一行與 `pragma solidity ^0.8.24;` 版本指示。pragma 告訴編譯器你是針對哪個語言版本寫的;^0.8 很關鍵,因為從 0.8.0 起算術溢位會自動回滾,而不再悄悄繞回(wrap)。
  2. `contract Counter { ... }` 宣告這個合約——一束狀態與程式碼,將被部署到單一位址。可以把它想成一個類別,而它唯一的實例就活在鏈上。
  3. `uint256 public count;` 是一個狀態變數:合約在每次呼叫之間都會記住的一塊資料。
  4. `constructor`(建構子)在部署交易期間只執行一次,用來設定初始值——之後它就消失,永遠不會再被執行。
  5. 那四個 `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
使用 Foundry 的 cast:`call` 是免費讀取(eth_call);`send` 是要付費、會改變狀態的交易。

可見性:誰有資格呼叫這個?

每個函式都帶有一個可見性關鍵字,決定可以呼叫它。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;
    }
}
四種可見性層級,依觸及範圍由窄到寬。
  1. `public`(公開)——敞開的門。可從鏈外呼叫,也可被合約內其他函式呼叫。狀態變數也可以是 public(就是先前那個自動 getter)。
  2. `external`(外部)——只能從外部呼叫。你無法在同一合約的另一個函式中直接呼叫 external 函式,除非加上 `this.` 前綴。它是專供使用者與其他合約使用的進入點的自然選擇。
  3. `internal`(內部)——可供本合約以及任何繼承它的合約使用;對外界不可見。最適合用來放共用的輔助邏輯。
  4. `private`(私有)——只有這個合約本身能用,連繼承它的子合約都不行。是最緊的範圍。

部署的生命週期,以及接下來

  1. 寫好 Solidity 原始碼,然後編譯。編譯器會產出兩樣東西:EVM 位元組碼(真正執行的東西)與 ABI(應用程式用來與它對話的呼叫慣例)。
  2. 送出一筆攜帶該位元組碼的部署交易。EVM 會把你的建構子恰好執行一次、設定初始狀態,並把執行期位元組碼存到一個全新鑄出的合約位址。
  3. 自此之後,使用者與其他合約透過向該位址送出交易(用於改變狀態,要付 gas)或 eth_call(用於免費讀取)來互動。

你現在已經握有每個合約共有的骨架:存放在 storage 的狀態變數、只跑一次的建構子、由可見性把關的函式,以及把免費讀取與付費寫入分開的 `view`/`pure` 標記。從這裡開始,合約會長出真正的肌肉。下一篇會加入更豐富的資料——型別,而 修飾器緊接著作為護欄登場,再加上用來描述餘額與紀錄的映射與結構——再過幾篇,你就會把這一切組裝成一個可運作的 ERC-20 代幣。