Solidity 智慧合約開發基礎

Solidity 是以太坊智慧合約的主要程式語言,專為區塊鏈上的去中心化應用設計。自 2014 年首次發布以來,Solidity 已經成為智慧合約開發的業界標準,被廣泛應用於 DeFi、NFT、DAO 等各種區塊鏈應用。

Solidity 智慧合約開發基礎

概述

Solidity 是以太坊智慧合約的主要程式語言,專為區塊鏈上的去中心化應用設計。自 2014 年首次發布以來,Solidity 已經成為智慧合約開發的業界標準,被廣泛應用於 DeFi、NFT、DAO 等各種區塊鏈應用。

本文從基礎語法到實際開發,逐步引導讀者掌握 Solidity 的核心概念與開發技巧。

開發環境設置

本地開發環境

1. 安裝 Node.js 與 npm

Solidity 開發通常使用 Hardhat 或 Foundry 框架,這些工具基於 Node.js 環境:

# 使用 nvm 安裝 Node.js LTS 版本
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.0/install.sh | bash
nvm install 18
nvm use 18

# 驗證安裝
node --version
npm --version

2. 安裝 Hardhat

Hardhat 是最流行的以太坊開發環境:

# 建立專案目錄
mkdir my-solidity-project
cd my-solidity-project

# 初始化 npm
npm init -y

# 安裝 Hardhat
npm install --save-dev hardhat

# 初始化 Hardhat 專案
npx hardhat init

選擇「Create a JavaScript project」開始一個基本專案。

3. 安裝 Foundry(可選)

Foundry 以其高速測試著稱:

# 安裝 Foundry
curl -L https://foundry.paradigm.xyz | bash
foundryup

# 初始化專案
forge init my-project

線上開發環境

Remix IDE

Remix 是瀏覽器版的 Solidity IDE,無需本地安裝:

  1. 訪問 https://remix.ethereum.org
  2. 建立新檔案(.sol 副檔名)
  3. 編譯並部署

Solidity 基礎語法

版本聲明

每個 Solidity 合約必須聲明編譯器版本:

// 精確版本
pragma solidity 0.8.19;

// 版本範圍
pragma solidity ^0.8.0;      // >= 0.8.0 且 < 0.9.0
pragma solidity >=0.8.0 <0.9.0;

// 多版本範圍
pragma solidity ^0.8.19;

基本資料型態

數值型態

// 整數
uint8   // 無符號 8 位元整數:0 到 2^8-1
uint256 // 無符號 256 位元整數:0 到 2^256-1(最常用)
int8    // 有符號 8 位元整數:-2^7 到 2^7-1
int256  // 有符號 256 位元整數

// 地址
address     // 20 位元組地址
address payable // 可接收 ETH 的地址

// 位元組
bytes1  bytes32 // 固定長度位元組陣列
bytes             // 動態長度位元組陣列

// 字串
string // UTF-8 字串

// 布林值
bool // true 或 false

變數宣告

// 狀態變數(儲存在區塊鏈上)
contract Example {
    uint256 public count = 0;           // 公開狀態變數
    address public owner;               // 地址類型
    mapping(address => uint256) balances; // 映射
    bool public isActive = true;
    bytes32 public dataHash;

    // 建構函數
    constructor() {
        owner = msg.sender;
    }
}

// 局部變數(函數內使用)
function foo() public pure returns (uint256) {
    uint256 localVar = 10;
    bool flag = true;
    address sender = msg.sender;
    return localVar;
}

函數結構

// 基本函數結構
function functionName(parameters) [visibility] [stateMutability] [returns(returnType)] {
    // 函數主體
}

// 完整範例
function transfer(address to, uint256 amount) public returns (bool) {
    require(balances[msg.sender] >= amount, "Insufficient balance");
    balances[msg.sender] -= amount;
    balances[to] += amount;
    emit Transfer(msg.sender, to, amount);
    return true;
}

可視性(Visibility)

contract Visibility {
    uint256 private privateVar;      // 僅合約內可存取
    uint256 internal internalVar;    // 合約及繼承合約可存取
    uint256 public publicVar;        // 外部與內部都可存取(自動生成 getter)

    // 外部函數:只能透過交易呼叫
    function externalFunc() external view returns (uint256) {
        return privateVar; // 可存取
    }

    // 內部函數:合約內與繼承合約可呼叫
    function internalFunc() internal view returns (uint256) {
        return internalVar;
    }

    // 公開函數:內外部都可呼叫
    function publicFunc() public pure returns (string memory) {
        return "public";
    }

    // 私有函數:僅合約內可呼叫
    function privateFunc() private pure returns (bool) {
        return true;
    }
}

狀態可變性(State Mutability)

contract Mutability {
    // pure:完全不讀寫狀態
    function add(uint256 a, uint256 b) public pure returns (uint256) {
        return a + b;
    }

    // view:讀取狀態但不修改
    function getBalance(address account) public view returns (uint256) {
        return account.balance;
    }

    // payable:可接收 ETH
    function deposit() public payable returns (uint256) {
        return msg.value;
    }

    // 預設:可讀寫狀態
    uint256 public count;
    function increment() public {
        count++;
    }
}

控制流語句

// 條件判斷
function check(uint256 x) public pure returns (string memory) {
    if (x > 10) {
        return "big";
    } else if (x > 5) {
        return "medium";
    } else {
        return "small";
    }
}

// 迴圈(需注意 Gas 限制)
function sum(uint256 n) public pure returns (uint256) {
    uint256 total = 0;
    for (uint256 i = 1; i <= n; i++) {
        total += i;
    }
    return total;
}

// require 用於錯誤處理
function withdraw(uint256 amount) public {
    require(amount > 0, "Amount must be greater than 0");
    require(balances[msg.sender] >= amount, "Insufficient balance");
    // ...
}

// revert 用於主動回滾
function transfer(address to, uint256 amount) public {
    if (balances[msg.sender] < amount) {
        revert("Insufficient balance");
    }
    // ...
}

映射(Mapping)與陣列

映射(Mapping)

Mapping 是 Solidity 中最重要的資料結構之一:

contract MappingExample {
    // 基本映射
    mapping(address => uint256) public balances;

    // 巢狀映射
    mapping(address => mapping(address => uint256)) public allowances;

    // 映射操作
    function setBalance(address account, uint256 amount) public {
        balances[account] = amount;
    }

    function getBalance(address account) public view returns (uint256) {
        return balances[account];
    }

    // 映射無法直接迭代,需搭配陣列
    address[] public accountList;

    function addAccount(address account) public {
        accountList.push(account);
    }

    function getAllBalances() public view returns (uint256[] memory) {
        uint256[] memory result = new uint256[](accountList.length);
        for (uint256 i = 0; i < accountList.length; i++) {
            result[i] = balances[accountList[i]];
        }
        return result;
    }
}

陣列

contract ArrayExample {
    // 動態陣列
    uint256[] public dynamicArray;
    address[] public addressArray;

    // 固定長度陣列
    uint256[5] public fixedArray;

    // 陣列操作
    function arrayOps() public {
        // push:新增元素
        dynamicArray.push(1);
        dynamicArray.push(2);

        // length:取得長度
        uint256 len = dynamicArray.length;

        // 存取元素
        uint256 first = dynamicArray[0];

        // pop:移除最後元素
        dynamicArray.pop();

        // 刪除(重置為預設值)
        delete dynamicArray[0];
        delete dynamicArray; // 重置整個陣列
    }

    // 結構體陣列
    struct Person {
        string name;
        uint256 age;
    }

    Person[] public people;

    function addPerson(string memory _name, uint256 _age) public {
        people.push(Person(_name, _age));

        // 或使用具名參數
        people.push(Person({name: _name, age: _age}));
    }
}

結構體與列舉

結構體(Struct)

contract StructExample {
    struct Token {
        string name;
        string symbol;
        uint256 totalSupply;
        mapping(address => uint256) balances;
    }

    // 建立結構體變數
    Token public token;

    // 初始化結構體
    function initToken() public {
        token = Token({
            name: "My Token",
            symbol: "MTK",
            totalSupply: 1000000
        });
    }

    // 使用 mapping 搭配結構體
    mapping(address => Token[]) public userTokens;

    function mint(address to, string memory name, string memory symbol) public {
        Token memory newToken = Token({
            name: name,
            symbol: symbol,
            totalSupply: 0
        });
        userTokens[to].push(newToken);
    }
}

列舉(Enum)

contract EnumExample {
    enum Status { Pending, Active, Completed, Cancelled }

    Status public currentStatus;

    function setStatus(Status _status) public {
        currentStatus = _status;
    }

    function complete() public {
        currentStatus = Status.Completed;
    }

    function isCompleted() public view returns (bool) {
        return currentStatus == Status.Completed;
    }
}

事件與日誌

事件(Event)

事件是用於記錄區塊鏈狀態變更的機制:

contract EventExample {
    // 定義事件
    event Transfer(address indexed from, address indexed to, uint256 value);
    event Approval(address indexed owner, address indexed spender, uint256 value);

    // 映射餘額
    mapping(address => uint256) public balances;
    mapping(address => mapping(address => uint256)) public allowances;

    // 發送代幣
    function transfer(address to, uint256 amount) public returns (bool) {
        require(balances[msg.sender] >= amount, "Insufficient balance");

        balances[msg.sender] -= amount;
        balances[to] += amount;

        // 發送事件
        emit Transfer(msg.sender, to, amount);
        return true;
    }

    // 授權
    function approve(address spender, uint256 amount) public returns (bool) {
        allowances[msg.sender][spender] = amount;
        emit Approval(msg.sender, spender, amount);
        return true;
    }

    // 轉讓(使用授權額度)
    function transferFrom(address from, address to, uint256 amount) public returns (bool) {
        require(balances[from] >= amount, "Insufficient balance");
        require(allowances[from][msg.sender] >= amount, "Allowance exceeded");

        balances[from] -= amount;
        balances[to] += amount;
        allowances[from][msg.sender] -= amount;

        emit Transfer(from, to, amount);
        return true;
    }
}

indexed 關鍵字

// indexed 參數可用於篩選事件
event Transfer(
    address indexed from,   // 可被索引
    address indexed to,     // 可被索引
    uint256 value          // 不可被索引
);

// 在客戶端篩選特定地址的轉帳
// contract.Transfer({ from: "0x123..." })

繼承與修飾符

繼承

// 基底合約
contract Ownable {
    address public owner;

    event OwnershipTransferred(address indexed previousOwner, address indexed newOwner);

    constructor() {
        owner = msg.sender;
    }

    modifier onlyOwner() {
        require(msg.sender == owner, "Not the owner");
        _;
    }

    function transferOwnership(address newOwner) public onlyOwner {
        require(newOwner != address(0), "Invalid address");
        emit OwnershipTransferred(owner, newOwner);
        owner = newOwner;
    }
}

// 繼承合約
contract MyToken is Ownable {
    string public name = "My Token";
    string public symbol = "MTK";
    uint256 public totalSupply;

    mapping(address => uint256) public balances;

    function mint(address to, uint256 amount) public onlyOwner {
        balances[to] += amount;
        totalSupply += amount;
    }
}

多重繼承

contract Base1 {
    function foo() public pure virtual returns (string memory) {
        return "Base1";
    }
}

contract Base2 {
    function foo() public pure virtual returns (string memory) {
        return "Base2";
    }
}

// 線性繼承順序:Derived -> Base2 -> Base1
contract Derived is Base2, Base1 {
    // 如果 Base1 和 Base2 都有 foo,需覆蓋
    function foo() public pure override(Base2, Base1)) {
        return returns (string memory "Derived";
    }
}

修飾符(Modifier)

contract ModifierExample {
    address public owner;
    uint256 public count = 0;
    mapping(address => uint256) public lastAction;

    constructor() {
        owner = msg.sender;
    }

    // 簡單修飾符
    modifier onlyOwner() {
        require(msg.sender == owner, "Not owner");
        _;
    }

    // 帶參數的    modifier only修飾符
After(uint256 time) {
        require(block.timestamp >= time, "Too early");
        _;
    }

    // 檢查並更新狀態的修飾符
    modifier rateLimit() {
        require(
            block.timestamp > lastAction[msg.sender] + 1 minutes,
            "Rate limit exceeded"
        );
        _;
        lastAction[msg.sender] = block.timestamp;
    }

    function increment() public onlyOwner rateLimit {
        count++;
    }

    function delayedAction() public onlyAfter(1234567890) {
        // ...
    }
}

介面與函式庫

介面(Interface)

// ERC-20 代幣介面
interface IERC20 {
    function totalSupply() external view returns (uint256);
    function balanceOf(address account) external view returns (uint256);
    function transfer(address to, 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 from, address to, 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);
}

// 使用介面
contract TokenSwap {
    function swap(address tokenA, address tokenB, uint256 amount) public {
        IERC20(tokenA).transferFrom(msg.sender, address(this), amount);
        // ...
    }
}

函式庫(Library)

// 數學函式庫
library Math {
    function min(uint256 a, uint256 b) internal pure returns (uint256) {
        return a < b ? a : b;
    }

    function max(uint256 a, uint256 b) internal pure returns (uint256) {
        return a > b ? a : b;
    }
}

// 使用函式庫
contract UseLibrary {
    function testMath(uint256 a, uint256 b) public pure returns (uint256) {
        return Math.min(a, b);
    }
}

// 使用 using for
library Address {
    function isContract(address account) internal view returns (bool) {
        uint256 size;
        assembly {
            size := extcodesize(account)
        }
        return size > 0;
    }
}

contract UseAddress {
    using Address for address;

    function checkContract(address addr) public view returns (bool) {
        return addr.isContract();
    }
}

ERC 標準

ERC-20:代幣標準

// 簡化的 ERC-20 實現
contract SimpleToken is IERC20 {
    string public name = "Simple Token";
    string public symbol = "SIM";
    uint8 public decimals = 18;
    uint256 public totalSupply;

    mapping(address => uint256) public balanceOf;
    mapping(address => mapping(address => uint256)) public allowance;

    function transfer(address to, uint256 amount) public override returns (bool) {
        require(balanceOf[msg.sender] >= amount, "Insufficient balance");
        balanceOf[msg.sender] -= amount;
        balanceOf[to] += amount;
        emit Transfer(msg.sender, to, amount);
        return true;
    }

    function approve(address spender, uint256 amount) public override returns (bool) {
        allowance[msg.sender][spender] = amount;
        emit Approval(msg.sender, spender, amount);
        return true;
    }

    function transferFrom(address from, address to, uint256 amount) public override returns (bool) {
        require(balanceOf[from] >= amount, "Insufficient balance");
        require(allowance[from][msg.sender] >= amount, "Allowance exceeded");

        balanceOf[from] -= amount;
        balanceOf[to] += amount;
        allowance[from][msg.sender] -= amount;

        emit Transfer(from, to, amount);
        return true;
    }

    function mint(address to, uint256 amount) public {
        balanceOf[to] += amount;
        totalSupply += amount;
        emit Transfer(address(0), to, amount);
    }
}

ERC-721:NFT 標準

// ERC-721 核心介面
interface IERC721 {
    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);
    function ownerOf(uint256 tokenId) external view returns (address);
    function safeTransferFrom(address from, address to, uint256 tokenId, bytes calldata data) external;
    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 setApprovalForAll(address operator, bool approved) external;
    function getApproved(uint256 tokenId) external view returns (address);
    function isApprovedForAll(address owner, address operator) external view returns (bool);
}

常見安全漏洞與防範

1. 重入攻擊

// 有漏洞的合約
contract Vulnerable {
    mapping(address => uint256) public balances;

    function withdraw() public {
        (bool success, ) = msg.sender.call{value: balances[msg.sender]}("");
        balances[msg.sender] = 0; // 在呼叫後修改狀態!
    }
}

// 安全版本:Checks-Effects-Interactions 模式
contract Secure {
    mapping(address => uint256) public balances;

    function withdraw() public {
        uint256 amount = balances[msg.sender];
        require(amount > 0, "No balance");

        // Checks
        // Effects(先修改狀態)
        balances[msg.sender] = 0;

        // Interactions(最後與其他合約互動)
        (bool success, ) = msg.sender.call{value: amount}("");
        require(success, "Transfer failed");
    }
}

2. 整數溢位

// 使用 SafeMath 庫(舊版本)
import "@openzeppelin/contracts/utils/SafeMath.sol";

contract WithSafeMath {
    using SafeMath for uint256;

    function add(uint256 a, uint256 b) public pure returns (uint256) {
        return a.add(b);
    }
}

// Solidity 0.8+ 內建溢位檢查
contract Solidity08Safe {
    function add(uint256 a, uint256 b) public pure returns (uint256) {
        return a + b; // 自動檢查溢位,失敗會 revert
    }

    // 如需繞過檢查
    function addUnchecked(uint256 a, uint256 b) public pure returns (uint256) {
        unchecked {
            return a + b;
        }
    }
}

3. 存取控制

// 錯誤:初始化函數可被任何人呼叫
contract BadInit {
    address public owner;
    bool public initialized;

    function init() public {
        require(!initialized);
        owner = msg.sender;
        initialized = true;
    }
}

// 正確:使用 constructor
contract GoodInit {
    address public owner;

    constructor() {
        owner = msg.sender;
    }
}

// 或使用 initializer 修飾符(OpenZeppelin)
contract Initializable {
    bool private initialized;

    modifier initializer() {
        require(!initialized, "Already initialized");
        _;
        initialized = true;
    }

    function init() public initializer {
        // 初始化邏輯
    }
}

測試與部署

Hardhat 測試

// test/sample-test.js
const { expect } = require("chai");

describe("Token", function () {
  let token;
  let owner;
  let addr1;

  beforeEach(async function () {
    const Token = await ethers.getContractFactory("MyToken");
    [owner, addr1] = await ethers.getSigners();
    token = await Token.deploy();
    await token.deployed();
  });

  it("Should assign total supply to owner", async function () {
    const ownerBalance = await token.balanceOf(owner.address);
    expect(await token.totalSupply()).to.equal(ownerBalance);
  });

  it("Should transfer tokens between accounts", async function () {
    await token.transfer(addr1.address, 50);
    expect(await token.balanceOf(addr1.address)).to.equal(50);
  });
});

部署到測試網路

# 部署到 Sepolia 測試網路
npx hardhat run scripts/deploy.js --network sepolia

# 或使用 Hardhat Ignition
npx hardhat ignition deploy

開發最佳實踐

1. 程式碼組織

2. Gas 優化

3. 安全檢查清單

4. 文件記錄

/// @title Simple Storage
/// @author Your Name
/// @notice 儲存簡單數值的合約
/// @dev 使用 mapping 儲存資料
contract SimpleStorage {
    /// @notice 儲存一個數值
    /// @param value 要儲存的數值
    function set(uint256 value) external {
        // ...
    }
}

總結

Solidity 作為以太坊智慧合約的主要開發語言,其語法與 JavaScript 相似但包含區塊鏈特有的概念:Gas 成本、狀態儲存、與不可變性。掌握這些概念對於開發安全可靠的智慧合約至關重要。

本文介紹了 Solidity 的核心語法,包括變數、函數、控制流、資料結構、繼承、介面、以及安全最佳實踐。透過不斷練習與實際專案經驗,開發者可以逐漸精通這門語言並構建各類創新的去中心化應用。

建議開發者持續關注以太坊改進提案(EIP)的動態,因為 Solidity 語言與 EVM 都在持續演進。同時,遵循安全最佳實踐並使用經過審計的開源庫(如 OpenZeppelin)是構建生產級智慧合約的關鍵。

延伸閱讀與來源

這篇文章對您有幫助嗎?

評論

發表評論

注意:由於這是靜態網站,您的評論將儲存在本地瀏覽器中,不會公開顯示。

目前尚無評論,成為第一個發表評論的人吧!