在 Web3 的世界裡,每一行程式碼都與「錢(加密資產)」直接掛鉤,且佈署後無法輕易竄改。因此,今天我們在解析 Solidity 的基礎語法時,我不會只教你怎麼宣告變數,而是會特別著重在那些容易引發資安漏洞的語法特性上。
- 狀態變數與儲存位置 (Storage vs. Memory)
在 Solidity 中,變數宣告的位置決定了它會被存在哪裡,這不僅影響 Gas 費用,更與合約狀態息息相關。
- 狀態變數 (State Variables):宣告在合約層級(不在任何函數內)的變數。這些變數會被永久寫入區塊鏈的「Storage」中。改動它們需要消耗極高的 Gas。
- 區域變數 (Local Variables):宣告在函數內部的變數。通常只在函數執行期間存在記憶體「Memory」中,執行完畢就消失,消耗的 Gas 極少。
資安視角:許多初學者在處理陣列或字串時,會搞混 storage 和 memory 的指標參考(Pointer),導致不小心覆寫了區塊鏈上的重要全域狀態變數,這是一種極度危險的潛在漏洞。
- 特殊的型別系統
除了常見的布林值 (bool) 和字串 (string),Solidity 有幾個為區塊鏈量身打造的特殊型別:
整數型別 (uint / int)在區塊鏈上最常處理的就是代幣數量,因此我們幾乎都是使用無號整數 uint(預設為 uint256,代表 256 位元)。
資安視角:在 Solidity 0.8.0 版本之前,整數如果超過最大值會發生「溢位 (Overflow)」而歸零,這曾是 DeFi 圈最氾濫的漏洞之一(我們將在 Day 16 實戰這個漏洞)。雖然新版 Solidity 內建了溢位檢查,但型別的邊界值依然是稽核時的重點。
地址型別 (address)
這是 Solidity 最獨特的型別,用來儲存長度為 20 bytes 的以太坊地址。
更特別的是,它分為兩種:
- address:普通的地址。
- address payable:可以「接收以太幣」的地址。只有被標記為 payable 的地址,才能呼叫內建的 .transfer() 或 .send() 函數把錢轉進去。
- 可見度修飾子 (Visibility Modifiers)
這絕對是 Solidity 資安防護的第一道防線!當你宣告一個函數時,必須明確指定誰有權限呼叫它:
- public:任何人(外部帳戶或其他合約)都可以呼叫,也會自動產生狀態變數的讀取函數。
- private:只有當前這個合約內部可以呼叫(即使是繼承它的子合約也不行)。
- internal:當前合約與繼承它的子合約可以呼叫(類似保護級別)。
- external:只能從合約外部呼叫,合約內部的其他函數不能直接呼叫它(但可以透過 this.func() 呼叫)。
資安視角: 史上最著名的「Parity 錢包凍結事件」(損失數千萬美元),起因就是開發者忘記把一個極度機敏的初始化函數加上 internal 修飾子,結果它預設變成了 public,被駭客從外部直接呼叫並奪取了合約的所有權!
- 基礎架構範例
讓我們把上述觀念結合成一個簡單的範例:
'''
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20; // 宣告編譯器版本
contract SimpleBank {
// 狀態變數 (Storage)
address public owner;
mapping(address => uint256) private balances; // 紀錄每個地址的餘額,設為 private 保護隱私
// 建構子:合約部署時只會執行一次
constructor() {
owner = msg.sender; // msg.sender 是一個全域變數,代表呼叫此函數的人
}
// 存款函數:必須標記 payable 才能接收以太幣
function deposit() public payable {
// msg.value 代表這次交易夾帶的以太幣數量
balances[msg.sender] += msg.value;
}
// 查詢餘額:view 代表這個函數只讀取、不修改區塊鏈狀態(免 Gas)
function getBalance() public view returns (uint256) {
return balances[msg.sender];
}
}
'''
寫 Solidity 就像在設計一台防彈保險箱,你必須對每一個變數的儲存位置、每一個函數的存取權限錙銖必較。