以太坊智能合約開發實作教程:從環境搭建到部署上線完整指南

本教程帶領讀者從零開始,建立完整的以太坊開發環境,撰寫第一個智能合約,並將其部署到測試網絡和主網。我們使用 Solidity 作為合約語言,Hardhat 作為開發框架,提供一步一步的詳細操作指南。內容涵蓋 Hardhat 專案初始化、ERC-20 代幣合約撰寫、單元測試、Sepolia 測試網絡部署、以及主網部署等完整流程。

以太坊智能合約開發實作教程:從環境搭建到部署上線完整指南

概述

以太坊智能合約開發是區塊鏈工程師最重要的核心技能之一。本教程將帶領讀者從零開始,建立完整的以太坊開發環境,撰寫第一個智能合約,並將其部署到測試網絡和主網。我們將使用 Solidity 作為合約語言,Hardhat 作為開發框架,提供一步一步的詳細操作指南。

本教程適合具備基本程式設計經驗的開發者,無需區塊鏈開發經驗。我們將涵蓋開發環境設定、合約撰寫、測試、部署等完整流程,並提供詳細的代碼範例和解釋。通過本教程,讀者將能夠獨立開發和部署基本的以太坊智能合約。

一、开发环境搭建

1.1 必要的軟體工具

在開始開發以太坊智能合約之前,需要準備以下軟體環境:

Node.js 與 npm

Solidity 合約的開發和測試需要 Node.js 環境。建議使用 Node.js 18.x 或更高版本,以及 npm 9.x 或更高版本。

# 檢查 Node.js 版本
node --version

# 檢查 npm 版本
npm --version

# 如果未安裝,使用 nvm 安裝(推薦)
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.0/install.sh | bash
nvm install 20
nvm use 20

Git 版本控制

Git 是程式碼管理的必備工具。

# 檢查 Git 版本
git --version

# 配置 Git
git config --global user.name "Your Name"
git config --global user.email "your.email@example.com"

代碼編輯器

推薦使用 Visual Studio Code(VS Code),並安裝以下擴充套件:

1.2 Hardhat 專案初始化

Hardhat 是以太坊生態系統中最流行的開發框架,提供了編譯、測試、部署等完整功能。

創建新專案

# 創建專案目錄
mkdir my-ethereum-dapp
cd my-ethereum-dapp

# 初始化 npm 專案
npm init -y

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

# 初始化 Hardhat 專案
npx hardhat init

執行 npx hardhat init 後,會出現互動式選單,選擇「Create a JavaScript project」或「Create a TypeScript project」。本教程使用 TypeScript。

Hardhat 專案初始化選單:

? What do you want to do? …
  ▸ Create a JavaScript project
    Create a TypeScript project
    Create an empty hardhat.config.js
    Quit

選擇項目類型後,Hardhat 會自動建立以下目錄結構:

my-ethereum-dapp/
├── contracts/           # 智能合約原始碼
├── scripts/            # 部署腳本
├── test/               # 測試檔案
├── hardhat.config.ts   # Hardhat 設定
├── package.json        # 專案依賴
└── tsconfig.json       # TypeScript 設定

1.3 必要的依賴套件

除了 Hardhat 核心,我們還需要安裝其他必要的依賴:

# 安裝 OpenZeppelin 合約庫(安全的合約開發庫)
npm install @openzeppelin/contracts

# 安裝 ethers.js(以太坊交互庫)
npm install ethers

# 安裝 chai(測試框架)
npm install --save-dev chai

# 安裝 typechain(TypeScript 類型生成)
npm install --save-dev typechain @typechain/hardhat @typechain/ethers-v6

1.4 Hardhat 配置文件

編輯 hardhat.config.ts 檔案,配置網絡和編譯器設定:

import { HardhatUserConfig } from "hardhat/config";
import "@nomicfoundation/hardhat-toolbox";
import "@typechain/hardhat";

const config: HardhatUserConfig = {
  solidity: {
    version: "0.8.26",
    settings: {
      optimizer: {
        enabled: true,
        runs: 200,
      },
    },
  },
  networks: {
    // 本地測試網絡
    localhost: {
      url: "http://127.0.0.1:8545",
    },
    // Sepolia 測試網絡
    sepolia: {
      url: process.env.SEPOLIA_RPC_URL || "",
      accounts: process.env.PRIVATE_KEY ? [process.env.PRIVATE_KEY] : [],
      chainId: 11155111,
    },
    // 主網
    mainnet: {
      url: process.env.MAINNET_RPC_URL || "",
      accounts: process.env.PRIVATE_KEY ? [process.env.PRIVATE_KEY] : [],
      chainId: 1,
    },
  },
  // Etherscan 合約驗證
  etherscan: {
    apiKey: process.env.ETHERSCAN_API_KEY || "",
  },
};

export default config;

1.5 環境變量設定

創建 .env 檔案存放敏感資訊:

# 錢包私鑰(不要提交到版本控制!)
PRIVATE_KEY=your_private_key_here

# RPC URL
SEPOLIA_RPC_URL=https://sepolia.infura.io/v3/YOUR_INFURA_KEY
MAINNET_RPC_URL=https://mainnet.infura.io/v3/YOUR_INFURA_KEY

# Etherscan API Key(用於合約驗證)
ETHERSCAN_API_KEY=your_etherscan_api_key

確保將 .env 加入 .gitignore

node_modules/
.env
artifacts/
cache/
typechain-types/

二、智能合約開發

2.1 第一個合約:簡單的代幣合約

讓我們從最基本的 ERC-20 代幣合約開始。這是一個功能完整的可交易代幣合約。

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.26;

// 引入 OpenZeppelin 的 ERC-20 實現
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import "@openzeppelin/contracts/access/Ownable.sol";

/**
 * @title MyToken
 * @dev 我的第一個 ERC-20 代幣合約
 * 
 * 這是一個簡單的代幣合約,繼承了 OpenZeppelin 的安全實現。
 * 總供應量為 1,000,000 代幣,精度為 18 位小數。
 */
contract MyToken is ERC20, Ownable {
    
    // 事件:用於記錄重要操作
    event TokensMinted(address indexed to, uint256 amount);
    event TokensBurned(address indexed from, uint256 amount);
    
    /**
     * @dev 合約構造函數
     * @param name 代幣名稱
     * @param symbol 代幣符號
     */
    constructor(
        string memory name,
        string memory symbol
    ) ERC20(name, symbol) Ownable(msg.sender) {
        // 鑄造初始供應量:1,000,000 * 10^18
        uint256 initialSupply = 1000000 * 10 ** decimals();
        _mint(msg.sender, initialSupply);
    }
    
    /**
     * @dev 管理者鑄造新代幣
     * @param to 接收地址
     * @param amount 鑄造數量
     */
    function mint(address to, uint256 amount) public onlyOwner {
        _mint(to, amount);
        emit TokensMinted(to, amount);
    }
    
    /**
     * @dev 銷毀代幣
     * @param amount 銷毀數量
     */
    function burn(uint256 amount) public {
        _burn(msg.sender, amount);
        emit TokensBurned(msg.sender, amount);
    }
    
    /**
     * @dev 批量轉帳
     * @param recipients 接收者地址數組
     * @param amounts 對應的轉帳數量數組
     */
    function batchTransfer(
        address[] calldata recipients,
        uint256[] calldata amounts
    ) public {
        require(
            recipients.length == amounts.length,
            "Recipients and amounts length mismatch"
        );
        require(recipients.length > 0, "Empty array");
        
        for (uint256 i = 0; i < recipients.length; i++) {
            transfer(recipients[i], amounts[i]);
        }
    }
}

2.2 合約解說

讓我們詳細解說這個合約的各個部分:

版本聲明

pragma solidity ^0.8.26;

這行指定了合約使用的 Solidity 版本。^0.8.26 表示可以使用 0.8.26 到(但不包括)0.9.0 的版本。選擇這個版本是因為它提供了重要的安全特性,如防止整數溢出。

引入依賴

import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import "@openzeppelin/contracts/access/Ownable.sol";

OpenZeppelin 提供了經過安全審計的合約庫。使用這些庫可以避免常見的安全漏洞。

合約繼承

contract MyToken is ERC20, Ownable {

這個合約繼承了 ERC20(標準代幣介面)和 Ownable(所有者管理)。繼承是 Solidity 中代碼複用的主要方式。

變數與函數

2.3 部署合約的腳本

創建部署腳本 scripts/deploy.ts

import { ethers } from "hardhat";

async function main() {
  console.log("開始部署 MyToken 合約...");
  
  // 獲取部署帳戶
  const [deployer] = await ethers.getSigners();
  console.log(`部署帳戶: ${deployer.address}`);
  console.log(`帳戶餘額: ${(await ethers.provider.getBalance(deployer.address)).toString()}`);
  
  // 部署合約
  const tokenName = "MyToken";
  const tokenSymbol = "MTK";
  
  const MyToken = await ethers.getContractFactory("MyToken");
  const token = await MyToken.deploy(tokenName, tokenSymbol);
  
  // 等待合約部署完成
  await token.waitForDeployment();
  const contractAddress = await token.getAddress();
  
  console.log(`合約已部署到: ${contractAddress}`);
  console.log(`代幣名稱: ${await token.name()}`);
  console.log(`代幣符號: ${await token.symbol()}`);
  console.log(`總供應量: ${await token.totalSupply()}`);
  
  // 驗證合約(僅在測試網絡上)
  if (process.env.ETHERSCAN_API_KEY) {
    console.log("等待區塊確認後進行驗證...");
    await new Promise(resolve => setTimeout(resolve, 30000)); // 等待 30 秒
    
    try {
      await hre.run("verify:verify", {
        address: contractAddress,
        constructorArguments: [tokenName, tokenSymbol],
      });
      console.log("合約驗證成功!");
    } catch (error) {
      console.log("驗證失敗(可能需要稍後再試):", error);
    }
  }
}

main()
  .then(() => process.exit(0))
  .catch((error) => {
    console.error(error);
    process.exit(1);
  });

2.4 編譯合約

使用 Hardhat 編譯 Solidity 合約:

npx hardhat compile

編譯成功後,會在 artifacts/ 目錄生成合約 ABI 和位元組碼。

輸出範例:

Compiling 1 files with 0.8.26
Compilation finished successfully

artifacts/
└── contracts/
    └── MyToken.sol/
        ├── MyToken.dbg.json
        └── MyToken.json  # 包含 ABI 和 Bytecode

三、合約測試

3.1 測試框架介紹

Hardhat 內建了測試框架,基於 JavaScript/TypeScript 和 ethers.js。測試檔案放在 test/ 目錄。

3.2 撰寫單元測試

創建測試檔 test/MyToken.ts

import { expect } from "chai";
import { ethers } from "hardhat";
import { MyToken } from "../typechain-types";

describe("MyToken", function () {
  let token: MyToken;
  let owner: any;
  let addr1: any;
  let addr2: any;

  // 在每個測試套件前部署合約
  beforeEach(async function () {
    [owner, addr1, addr2] = await ethers.getSigners();
    
    const MyToken = await ethers.getContractFactory("MyToken");
    token = await MyToken.deploy("MyToken", "MTK") as MyToken;
    await token.waitForDeployment();
  });

  describe("部署", function () {
    it("應該設置正確的代幣名稱", async function () {
      expect(await token.name()).to.equal("MyToken");
    });

    it("應該設置正確的代幣符號", async function () {
      expect(await token.symbol()).to.equal("MTK");
    });

    it("應該將總供應量分配給部署者", async function () {
      const ownerBalance = await token.balanceOf(owner.address);
      expect(await token.totalSupply()).to.equal(ownerBalance);
    });
  });

  describe("轉帳交易", function () {
    it("應該能夠轉帳代幣", async function () {
      // 從 owner 轉帳 50 代幣給 addr1
      await token.transfer(addr1.address, 50);
      expect(await token.balanceOf(addr1.address)).to.equal(50);
    });

    it("應該在轉帳後扣除轉帳者餘額", async function () {
      const initialBalance = await token.balanceOf(owner.address);
      await token.transfer(addr1.address, 50);
      expect(await token.balanceOf(owner.address)).to.equal(initialBalance - 50n);
    });

    it("餘額不足時應該失敗", async function () {
      const ownerBalance = await token.balanceOf(owner.address);
      await expect(
        token.transfer(addr1.address, ownerBalance + 1n)
      ).to.be.revertedWith("ERC20: transfer amount exceeds balance");
    });
  });

  describe("授權和轉帳", function () {
    it("應該能夠授權轉帳", async function () {
      await token.approve(addr1.address, 100);
      expect(await token.allowance(owner.address, addr1.address)).to.equal(100);
    });

    it("應該能夠執行授權轉帳", async function () {
      await token.approve(addr1.address, 100);
      await token.connect(addr1).transferFrom(owner.address, addr2.address, 50);
      expect(await token.balanceOf(addr2.address)).to.equal(50);
    });
  });

  describe("鑄造和銷毀", function () {
    it("所有者應該能夠鑄造新代幣", async function () {
      const initialSupply = await token.totalSupply();
      await token.mint(addr1.address, 100);
      expect(await token.totalSupply()).to.equal(initialSupply + 100n);
      expect(await token.balanceOf(addr1.address)).to.equal(100);
    });

    it("非所有者不能鑄造代幣", async function () {
      await expect(
        token.connect(addr1).mint(addr1.address, 100)
      ).to.be.revertedWithCustomError(token, "OwnableUnauthorizedAccount");
    });

    it("任何人應該能夠銷毀自己的代幣", async function () {
      await token.transfer(addr1.address, 100);
      const initialSupply = await token.totalSupply();
      
      await token.connect(addr1).burn(50);
      
      expect(await token.balanceOf(addr1.address)).to.equal(50);
      expect(await token.totalSupply()).to.equal(initialSupply - 50n);
    });
  });

  describe("批量轉帳", function () {
    it("應該能夠批量轉帳", async function () {
      const recipients = [addr1.address, addr2.address];
      const amounts = [10, 20];
      
      await token.batchTransfer(recipients, amounts);
      
      expect(await token.balanceOf(addr1.address)).to.equal(10);
      expect(await token.balanceOf(addr2.address)).to.equal(20);
    });

    it("參數長度不匹配時應該失敗", async function () {
      await expect(
        token.batchTransfer([addr1.address], [10, 20])
      ).to.be.revertedWith("Recipients and amounts length mismatch");
    });
  });
});

3.3 執行測試

npx hardhat test

成功執行後的輸出:

  MyToken
    部署
      ✓ 應該設置正確的代幣名稱
      ✓ 應該設置正確的代幣符號
      ✓ 應該將總供應量分配給部署者
    轉帳交易
      ✓ 應該能夠轉帳代幣
      ✓ 應該在轉帳後扣除轉帳者餘額
      ✓ 餘額不足時應該失敗
    授權和轉帳
      ✓ 應該能夠授權轉帳
      ✓ 應該能夠執行授權轉帳
    鑄造和銷毀
      ✓ 所有者應該能夠鑄造新代幣
      ✓ 非所有者不能鑄造代幣
      ✓ 任何人應該能夠銷毀自己的代幣
    批量轉帳
      ✓ 應該能夠批量轉帳
      ✓ 參數長度不匹配時應該失敗

  14 passing (2s)

3.4 進階測試:覆蓋率分析

安裝 solidity-coverage 來分析測試覆蓋率:

npm install --save-dev solidity-coverage
npx hardhat coverage

四、部署到測試網絡

4.1 獲取測試網絡 ETH

部署到 Sepolia 測試網絡需要 Sepolia ETH。可以通過以下途徑獲取:

  1. 水龍頭網站
  1. 測試網絡橋接

4.2 配置 Sepolia 網絡

確保 .env 檔案包含 Sepolia RPC URL。可以使用 Infura 或 Alchemy 服務:

# 安裝 dotenv
npm install dotenv

# 在 hardhat.config.ts 頂部引入
import * as dotenv from "dotenv";
dotenv.config();

4.3 部署到 Sepolia

npx hardhat run scripts/deploy.ts --network sepolia

成功部署後的輸出:

開始部署 MyToken 合約...
部署帳戶: 0x1234...abcd
帳戶餘額: 1000000000000000000
合約已部署到: 0x5678...efgh
代幣名稱: MyToken
代幣符號: MTK
總供應量: 1000000000000000000000000

4.4 驗證合約

部署完成後,可以在 Etherscan 上驗證合約:

  1. 打開 Sepolia Etherscan (https://sepolia.etherscan.io)
  2. 搜尋合約地址
  3. 點擊「Contract」標籤
  4. 選擇「Verify and Publish」
  5. 輸入合約原始碼

或者使用 Hardhat 自動驗證:

npx hardhat verify --network sepolia CONTRACT_ADDRESS "MyToken" "MTK"

五、部署到主網

5.1 主網部署前的檢查清單

在部署到以太坊主網之前,請確保完成以下檢查:

部署前檢查清單:

□ 完整測試覆蓋
  └── 執行測試並確保所有測試通過
  └── 檢查測試覆蓋率

□ 安全審計
  └── 請專業審計公司審計合約
  └── 修復所有發現的問題

□ 合約驗證
  └── 在測試網絡驗證合約原始碼
  └── 確認功能正常

□  Gas 估算
  └── 估算部署成本
  └── 確保有足夠的 ETH

□ 應急計劃
  └── 準備暫停合約的機制(如需要)
  └── 準備升級合約的機制(如需要)

5.2 估算 Gas 費用

在部署前估算 Gas 費用:

// 估算部署 Gas
async function estimateGas() {
  const MyToken = await ethers.getContractFactory("MyToken");
  const bytecode = MyToken.getDeploymentTransaction().data;
  const estimate = await ethers.provider.estimateGas({
    data: bytecode,
  });
  
  const gasPrice = await ethers.provider.getFeeData();
  const cost = estimate * gasPrice.gasPrice;
  
  console.log(`估算 Gas: ${estimate.toString()}`);
  console.log(`Gas 價格: ${ethers.formatEther(gasPrice.gasPrice)} ETH`);
  console.log(`預估費用: ${ethers.formatEther(cost)} ETH`);
}

5.3 部署到主網

npx hardhat run scripts/deploy.ts --network mainnet

注意:主網部署需要真實的 ETH,請確保:

  1. 私鑰正確(不要使用測試網絡的私鑰)
  2. 有足夠的 ETH 餘額支付 Gas
  3. 網絡連接穩定

5.4 部署後的任務

部署完成後,還需要完成以下任務:

  1. 驗證合約
   npx hardhat verify --network mainnet CONTRACT_ADDRESS "MyToken" "MTK"
  1. 更新文檔
  1. 設定監控

六、與合約交互

6.1 使用腳本與合約交互

創建交互腳本 scripts/interact.ts

import { ethers } from "hardhat";

async function main() {
  // 連接到已部署的合約
  const contractAddress = "CONTRACT_ADDRESS_HERE";
  const MyToken = await ethers.getContractFactory("MyToken");
  const token = MyToken.attach(contractAddress);
  
  // 獲取帳戶
  const [owner, addr1] = await ethers.getSigners();
  
  // 查詢餘額
  console.log(`Owner 餘額: ${await token.balanceOf(owner.address)}`);
  console.log(`addr1 餘額: ${await token.balanceOf(addr1.address)}`);
  
  // 轉帳
  const tx = await token.transfer(addr1.address, 100);
  await tx.wait();
  console.log("轉帳完成!");
  
  // 查詢新餘額
  console.log(`addr1 新餘額: ${await token.balanceOf(addr1.address)}`);
}

main()
  .then(() => process.exit(0))
  .catch((error) => {
    console.error(error);
    process.exit(1);
  });

6.2 使用 Hardhat Console 交互

Hardhat 提供了互動式控制台:

npx hardhat console --network localhost

在控制台中:

// 獲取合約
const token = await ethers.getContractAt("MyToken", "CONTRACT_ADDRESS");

// 調用函數
await token.name();
await token.symbol();
await token.totalSupply();

// 發送交易
await token.transfer("ADDRESS", 100);

6.3 前端集成

要在 Web 應用中與合約交互,可以使用 ethers.js 或 wagmi:

// 使用 ethers.js
import { ethers } from "ethers";

const provider = new ethers.BrowserProvider(window.ethereum);
const signer = await provider.getSigner();
const contract = new ethers.Contract(
  "CONTRACT_ADDRESS",
  ABI,
  signer
);

// 調用函數
const balance = await contract.balanceOf(userAddress);

// 發送交易
const tx = await contract.transfer(toAddress, amount);
await tx.wait();

七、最佳實踐與安全建議

7.1 智能合約安全清單

安全檢查清單:

□ 防止重入攻擊
   └── 使用 Checks-Effects-Interactions 模式
   └── 使用 ReentrancyGuard

□ 防止整數溢出
   └── 使用 Solidity 0.8+ 版本
   └── 或使用 SafeMath 庫

□ 權限控制
   └── 使用 Ownable 或 AccessControl
   └── 驗證調用者身份

□ 輸入驗證
   └── 驗證所有輸入參數
   └── 檢查邊界條件

□ 緊急停止機制
   └── 實現 Pauseable 合約
   └── 準備升級路徑

□ 事件記錄
   └── 記錄所有重要操作
   └── 便於監控和審計

7.2 Gas 優化技巧

// 不好的方式:多次存儲
for (uint i = 0; i < length; i++) {
    data[i] = i;  // 每次循環都要寫入存儲
}

// 好的方式:使用 memory
uint[] memory temp = new uint[](length);
for (uint i = 0; i < length; i++) {
    temp[i] = i;  // 在記憶體中操作
}
// 最後一次性寫入存儲

7.3 常見錯誤與解決方案

錯誤原因解決方案
"Insufficient balance"餘額不足檢查餘額或使用足夠餘額的帳戶
"Nonce too high"交易序號錯誤重置錢包或使用正確的 nonce
"Gas estimation failed"合約調用失敗檢查合約邏輯
"Contract not verified"合約未驗證在 Etherscan 驗證

結論

本教程涵蓋了以太坊智能合約開發的完整流程,從環境搭建到部署上線。通過實踐這些步驟,讀者應該能夠:

  1. 建立完整的 Hardhat 開發環境
  2. 撰寫符合 ERC-20 標準的智能合約
  3. 編寫完整的單元測試
  4. 將合約部署到測試網絡和主網
  5. 與已部署的合約進行交互

這只是區塊鏈開發的起點。建議讀者繼續探索:

持續學習和實踐是成為優秀區塊鏈開發者的關鍵。祝你在以太坊開發的旅程中取得成功!

延伸閱讀與來源

這篇文章對您有幫助嗎?

評論

發表評論

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

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