EVM Gas 計算完整指南:數學推導、實務計算與最佳化策略

本文從頭到尾把 Gas 計算的數學邏輯推導一遍,配上 Solidity、JavaScript、Python 三種語言的程式碼範例,讓你知道合約的 Gas 消耗是怎麼跑出來的。涵蓋 EIP-1559 費用模型、Opcode 執行成本、SSTORE 機制、跨合約呼叫、退款模型、以及實用的 Gas 最佳化技巧。

EVM Gas 計算完整指南:數學推導、實務計算與最佳化策略

我第一次認真算 Gas 的時候,是因為用戶抱怨我的合約交易費用太高。那時候我還天真地以為「不就是 21,000 Gas 基礎費加上點計算嗎」。後來才發現,我合約裡那個看起來無害的 for 迴圈,居然吃掉了好幾萬 Gas。

這篇文章把 Gas 計算的數學邏輯從頭到尾推導一遍,再配上程式碼讓你知道「在 Solidity 裡,這個數字是怎麼跑出來的」。不管你是要面試、備考以太坊開發認證,還是想寫出更省 Gas 的合約,這篇都能派上用場。

資料截止 2026 年 3 月,所有計算基於 EIP-1559 之後的修正版。


基礎概念:什麼是 Gas?

在以太坊網路中,Gas 是衡量「工作量的單位」。你可以把它想成汽車的汽油——執行智能合約需要消耗 Gas,而 Gas 需要用 ETH 來購買。

交易費用公式(EIP-1559 之後):

費用 = Gas 使用量 × (基礎費用 + 小費)

其中:
- 基礎費用(Base Fee):由網路根據供需自動調整
- 小費(Priority Fee):給驗證者的額外費用,可設為 0
- Gas 使用量(Gas Used):合約實際執行的 opcode 總消耗

基礎費用計算

EIP-1559 引入的基礎費用機制,會根據上一個區塊的 Gas 使用量自動調整:

目標 Gas 使用量 = 15,000,000(區塊上限的一半)

如果上一個區塊 Gas 使用量 > 目標:
    基礎費用 += 基礎費用 × (使用量 - 目標) / 目標 / 8
    
如果上一個區塊 Gas 使用量 < 目標:
    基礎費用 -= 基礎費用 × (目標 - 使用量) / 目標 / 8

這個公式的特點是:無論如何調整,每個區塊的基礎費用變化不會超過 ±12.5%。

Solidity 計算基礎費用:

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

contract GasCalculator {
    // 目標區塊 Gas 使用量(區塊上限的一半)
    uint256 public constant TARGET_GAS = 15_000_000;
    // 區塊上限
    uint256 public constant BLOCK_GAS_LIMIT = 30_000_000;
    
    function calculateNextBaseFee(
        uint256 currentBaseFee,
        uint256 parentGasUsed
    ) external pure returns (uint256 nextBaseFee) {
        if (parentGasUsed > TARGET_GAS) {
            // 區塊太滿,增加基礎費用
            uint256 gasDelta = parentGasUsed - TARGET_GAS;
            // 增加比例上限為 12.5%
            uint256 baseFeeChange = currentBaseFee * gasDelta / TARGET_GAS / 8;
            nextBaseFee = currentBaseFee + baseFeeChange;
        } else {
            // 區塊太空,降低基礎費用
            uint256 gasDelta = TARGET_GAS - parentGasUsed;
            uint256 baseFeeChange = currentBaseFee * gasDelta / TARGET_GAS / 8;
            nextBaseFee = currentBaseFee - baseFeeChange;
        }
        
        // 基礎費用最低為 1 Wei
        if (nextBaseFee < 1) {
            nextBaseFee = 1;
        }
        
        return nextBaseFee;
    }
}

JavaScript 即時 Gas 估算:

const { ethers } = require("ethers");

async function calculateGasFees() {
    const provider = new ethers.JsonRpcProvider("https://mainnet.infura.io/v3/YOUR_PROJECT_ID");
    
    // 獲取當前區塊的基礎費用
    const feeData = await provider.getFeeData();
    
    console.log("當前 Gas 費用數據:");
    console.log(`基礎費用 (Base Fee): ${feeData.baseFeePerGas ? ethers.formatUnits(feeData.baseFeePerGas, 'gwei') : 'N/A'} Gwei`);
    console.log(`最大優先費用 (Max PriorityFee): ${feeData.maxPriorityFeePerGas ? ethers.formatUnits(feeData.maxPriorityFeePerGas, 'gwei') : 'N/A'} Gwei`);
    console.log(`最大費用上限 (Max Fee): ${feeData.maxFeePerGas ? ethers.formatUnits(feeData.maxFeePerGas, 'gwei') : 'N/A'} Gwei`);
    
    // 計算實際費用
    const gasLimit = 21000; // 標準 ETH 轉帳
    const estimatedTotal = feeData.maxFeePerGas 
        ? gasLimit * feeData.maxFeePerGas
        : gasLimit * feeData.gasPrice;
    
    console.log(`預估總費用: ${ethers.formatEther(estimatedTotal)} ETH`);
    
    // 估算交易確認時間
    const currentGasPrice = await provider.getGasPrice();
    console.log(`當前 Gas Price: ${ethers.formatUnits(currentGasPrice, 'gwei')} Gwei`);
}

calculateGasFees().catch(console.error);

Python 版本:

from web3 import Web3
import time

def calculate_gas_fees():
    w3 = Web3(Web3.HTTPProvider("https://mainnet.infura.io/v3/YOUR_PROJECT_ID"))
    
    # 獲取費用數據
    fee_data = w3.eth.fee_history(1, 'latest')
    
    print("當前 Gas 費用數據:")
    print(f"區塊基礎費用: {fee_data['baseFeePerGas'][-1] / 10**9:.2f} Gwei")
    
    # 計算平均優先費用
    priority_fees = [int.from_bytes(f, 'big') for f in fee_data['reward'][-10:]]
    avg_priority = sum(priority_fees) / len(priority_fees) / 10**9
    print(f"平均優先費用: {avg_priority:.2f} Gwei")
    
    # 估算費用
    gas_limit = 21000  # 標準 ETH 轉帳
    base_fee = int(fee_data['baseFeePerGas'][-1])
    priority_fee = int(avg_priority * 10**9)
    
    total_fee = gas_limit * (base_fee + priority_fee)
    print(f"預估總費用: {w3.from_wei(total_fee, 'ether')} ETH")
    
    # 查詢網路負載
    block = w3.eth.get_block('latest')
    gas_used_ratio = block['gasUsed'] / block['gasLimit']
    print(f"區塊 Gas 使用率: {gas_used_ratio * 100:.1f}%")
    
    # 根據網路負載估算確認時間
    if gas_used_ratio > 0.9:
        print("網路擁堵,預計確認時間: 5-10 分鐘")
    elif gas_used_ratio > 0.5:
        print("網路正常,預計確認時間: 1-2 分鐘")
    else:
        print("網路順暢,預計確認時間: < 1 分鐘")

calculate_gas_fees()

交易 Gas 消耗分解

一筆標準 ETH 轉帳消耗 21,000 Gas。但智能合約交易的 Gas 消耗取決於很多因素。

基礎費用(Intrinsic Gas)

每筆交易都需要支付基礎費用:

基礎費用 = 21000 Gas(固定)+ calldata Gas

calldata Gas 計算:
- 每個非零位元組 = 16 Gas
- 每個零位元組 = 4 Gas
// 計算交易基礎費用(Solidity 示例)
function calculateIntrinsicGas(bytes memory data) public pure returns (uint256) {
    uint256 zeroBytes = 0;
    uint256 nonZeroBytes = 0;
    
    for (uint256 i = 0; i < data.length; i++) {
        if (data[i] == 0) {
            zeroBytes++;
        } else {
            nonZeroBytes++;
        }
    }
    
    // 基礎轉帳費用
    uint256 baseGas = 21000;
    
    // calldata 費用
    uint256 calldataGas = zeroBytes * 4 + nonZeroBytes * 16;
    
    return baseGas + calldataGas;
}

EVM Opcode 執行費用

合約內部的 Gas 消耗取決於執行的 Opcode。讓我推導幾個典型場景。

場景一:簡單變數讀寫

// 這段代碼的 Gas 分析
contract SimpleStorage {
    uint256 public value;
    
    // 讀取:~2,100 Gas (SLOAD cold)
    function read() public view returns (uint256) {
        return value;
    }
    
    // 寫入:~20,000 Gas (SSTORE 0→非0)
    function write(uint256 _value) external {
        value = _value;
    }
    
    // 讀取 + 寫入:~22,100 Gas
    function readAndWrite(uint256 _value) external returns (uint256) {
        uint256 oldValue = value;  // SLOAD cold: 2100
        value = _value;            // SSTORE 0→非0: 20000
        return oldValue;
    }
}

場景二:迴圈操作

contract LoopDemo {
    uint256[] public values;
    
    // 迴圈 Gas 消耗推導
    // 假設 length = n
    function sumValues() external view returns (uint256 sum) {
        // 讀取 length: SLOAD cold (2100 Gas)
        // 比較 n 次: n × JUMPI (3 Gas)
        // 加法 n 次: n × ADD (3 Gas)
        // 記憶體分配: MLOAD/MSTORE (3 Gas + memory cost)
        
        // n = 10 時預估:2100 + 30 + 30 + 30 = ~2200 Gas
        // n = 100 時預估:2100 + 300 + 300 + 記憶體成本
        
        for (uint256 i = 0; i < values.length; i++) {
            sum += values[i];
        }
    }
    
    // 優化版本:減少 SSTORE
    function sumValuesOptimized() external view returns (uint256 sum) {
        uint256 len = values.length; // 只 SLOAD 一次
        
        // 使用 assembly 優化迴圈
        assembly {
            mstore(0x00, 0) // 初始化 sum
            for { let i := 0 } lt(i, len) { i := add(i, 1) } {
                sum := add(sum, sload(add(values.slot, i)))
            }
        }
        
        return sum;
    }
}

JavaScript 實測 Gas 消耗:

const { ethers } = require("ethers");

async function measureLoopGas() {
    const provider = new ethers.JsonRpcProvider("https://mainnet.infura.io/v3/YOUR_PROJECT_ID");
    const signer = await provider.getSigner();
    
    const contractABI = [
        "function sumValues() view returns (uint256)",
        "function sumValuesOptimized() view returns (uint256)"
    ];
    const contractAddress = "YOUR_CONTRACT_ADDRESS";
    const contract = new ethers.Contract(contractAddress, contractABI, signer);
    
    // 設定測試資料
    const setDataTx = await contract.runner.sendTransaction({
        to: contractAddress,
        data: contract.interface.encodeFunctionData("values", [[1,2,3,4,5,6,7,8,9,10]])
    });
    await setDataTx.wait();
    
    // 估算 Gas(不發送交易)
    const gasNormal = await contract.sumValues.estimateGas();
    const gasOptimized = await contract.sumValuesOptimized.estimateGas();
    
    console.log(`普通版本 Gas: ${gasNormal}`);
    console.log(`優化版本 Gas: ${gasOptimized}`);
    console.log(`節省: ${gasNormal - gasOptimized} Gas (${((gasNormal - gasOptimized) / gasNormal * 100).toFixed(1)}%)`);
}

measureLoopGas().catch(console.error);

Python 版本:

from web3 import Web3

def measure_loop_gas():
    w3 = Web3(Web3.HTTPProvider("https://mainnet.infura.io/v3/YOUR_PROJECT_ID"))
    
    contract_address = "YOUR_CONTRACT_ADDRESS"
    abi = [
        {"inputs": [], "name": "sumValues", "outputs": [{"type": "uint256"}],
         "stateMutability": "view", "type": "function"},
        {"inputs": [], "name": "sumValuesOptimized", "outputs": [{"type": "uint256"}],
         "stateMutability": "view", "type": "function"}
    ]
    
    contract = w3.eth.contract(address=contract_address, abi=abi)
    
    # 估算 Gas
    gas_normal = contract.functions.sumValues().estimate_gas()
    gas_optimized = contract.functions.sumValuesOptimized().estimate_gas()
    
    print(f"普通版本 Gas: {gas_normal}")
    print(f"優化版本 Gas: {gas_optimized}")
    print(f"節省: {gas_normal - gas_optimized} Gas")
    
    # 計算費用差異
    gas_price = w3.eth.gas_price
    fee_diff_normal = gas_normal * gas_price
    fee_diff_optimized = gas_optimized * gas_price
    print(f"費用差異: {w3.from_wei(fee_diff_normal - fee_diff_optimized, 'ether')} ETH")

measure_loop_gas()

記憶體擴展成本模型

EIP-2028 之後,記憶體成本採用了新的計算模型:

C_mem(a) = 3 × a + floor(a² / 512)

其中 a = memoryUsageInWords = ceil(memoryBytes / 32)

這個模型的特點是:記憶體使用量小的時候,成本接近線性增長;當記憶體使用量變大,成本增長速度會越來越快。

記憶體成本推導表

記憶體大小(Bytes)│ Words │ 總 Gas 成本
─────────────────┼───────┼─────────────
0                │ 0     │ 0
32               │ 1     │ 3
64               │ 2     │ 6
128              │ 4     │ 14
256              │ 8     │ 38
512              │ 16    │ 98
1,024            │ 32    │ 266
2,048            │ 64    │ 716
4,096            │ 128   │ 2,342
8,192            │ 256   │ 7,414
16,384           │ 512   │ 22,874
32,768           │ 1024  │ 86,354

你可以看到,當記憶體從 1KB 增加到 32KB 時,成本從 266 Gas 暴漲到 86,354 Gas。

Solidity 記憶體成本實驗:

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

contract MemoryCostDemo {
    // 不同記憶體使用量的成本測試
    function allocateSmall() public pure returns (uint256) {
        uint256[1] memory arr; // 32 bytes
        arr[0] = 42;
        return arr[0];
    }
    
    function allocateMedium() public pure returns (uint256) {
        uint256[100] memory arr; // 3,200 bytes
        uint256 sum = 0;
        for (uint256 i = 0; i < 100; i++) {
            arr[i] = i;
            sum += arr[i];
        }
        return sum;
    }
    
    function allocateLarge() public pure returns (uint256) {
        uint256[1000] memory arr; // 32,000 bytes
        uint256 sum = 0;
        for (uint256 i = 0; i < 1000; i++) {
            arr[i] = i;
            sum += arr[i];
        }
        return sum;
    }
}

SSTORE 成本:狀態寫入的代價

SSTORE 是以太坊上最貴的操作之一。理解它的成本模型對 Gas 最佳化至關重要。

SSTORE 成本規則

將值從 0 寫成非0:20000 Gas
將值從非0 改成非0:2900 Gas
將值從非0 寫成 0:  2900 Gas - 退款 10000 Gas
將值從 0 寫成 0:  200 Gas

退款機制是為了獎勵「清理狀態」的行為,但退款上限是總消耗的一半。

實務推導

contract SSToreCostDemo {
    uint256 public value;
    uint256 public value2;
    
    // 場景1:首次寫入
    // Gas = 21000 (tx) + 2100 (cold SLOAD) + 20000 (SSTORE) = ~43100
    function firstWrite() external {
        value = 42; // 20000 Gas
    }
    
    // 場景2:更新現存值
    // Gas = 21000 (tx) + 2100 (cold SLOAD) + 100 (hot SLOAD) + 2900 (SSTORE) = ~24000
    function updateExisting() external {
        value = 100; // 2900 Gas (非0→非0)
    }
    
    // 場景3:清除值(獲得退款)
    // Gas = 21000 + 2100 + 100 + 2900 = 24100
    // 退還 = 10000 Gas
    // 實際净消耗 = 14100 Gas
    function clearValue() external {
        delete value; // 退還 10000 Gas
    }
    
    // 場景4:批量操作與退款上限
    // 如果一次性清除很多值,退款會被限制
    function batchClear() external {
        delete value;  // 退還 10000
        delete value2; // 退還 10000
        // 但總退款上限 = 總消耗 / 2
    }
}

跨合約呼叫成本

現代 DeFi 合約幾乎都會跨合約呼叫。理解這部分成本很重要。

CALL Opcode 成本

CALL 冷啟動:2600 Gas + memory cost
CALL 熱啟動:100 Gas + memory cost

被呼叫合約還需要支付自己的執行費用,這部分由呼叫方預付。

contract CrossContractDemo {
    // ERC20 代幣介面
    IERC20 public token;
    
    constructor(address _token) {
        token = IERC20(_token);
    }
    
    // 低效:每次迴圈都呼叫
    function batchTransferInefficient(
        address[] calldata recipients,
        uint256[] calldata amounts
    ) external returns (bool) {
        for (uint256 i = 0; i < recipients.length; i++) {
            // 每一次迴圈都是一次完整的 CALL
            token.transfer(recipients[i], amounts[i]);
        }
        return true;
    }
    
    // 高效:使用 multicall 批次
    function multicall(
        bytes[] calldata data
    ) external returns (bytes[] memory results) {
        results = new bytes[](data.length);
        for (uint256 i = 0; i < data.length; i++) {
            // 只執行一次外部呼叫
            (bool success, bytes memory result) = address(token).delegatecall(data[i]);
            require(success, "Call failed");
            results[i] = result;
        }
    }
}

JavaScript 批次呼叫優化:

const { ethers } = require("ethers");

async function optimizeCrossContractCalls() {
    const provider = new ethers.JsonRpcProvider("https://mainnet.infura.io/v3/YOUR_PROJECT_ID");
    const signer = await provider.getSigner();
    
    // Multicall3 合約(最常用的批次呼叫合約)
    const multicallAddress = "0xcA11bde05977b3631167028862bE2a173976CA11";
    const erc20ABI = [
        "function balanceOf(address) view returns (uint256)",
        "function transfer(address, uint256) returns (bool)"
    ];
    
    const multicall = new ethers.Contract(
        multicallAddress,
        ["function aggregate3(tuple(address target, bool allowFailure, bytes callData)[] calls) payable returns (tuple(bool success, bytes returnData)[])"],
        provider
    );
    
    const tokenAddress = "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48"; // USDC
    const userAddress = await signer.getAddress();
    
    // 批次查詢:5 個地址的代幣餘額
    const targets = [tokenAddress, tokenAddress, tokenAddress, tokenAddress, tokenAddress];
    const balanceOfIface = new ethers.Interface(erc20ABI);
    
    const calls = targets.map((target, i) => ({
        target,
        allowFailure: false,
        callData: balanceOfIface.encodeFunctionData("balanceOf", [userAddress])
    }));
    
    // 單一 RPC 呼叫獲取 5 個餘額
    console.log("執行批次查詢...");
    const startTime = Date.now();
    const results = await multicall.aggregate3.staticCall(calls);
    const endTime = Date.now();
    
    console.log(`批次查詢耗時: ${endTime - startTime}ms`);
    console.log(`節省約: ${(calls.length - 1) * 100}ms (相對於逐個查詢)`);
    
    // 解析結果
    results.forEach((result, i) => {
        const balance = ethers.formatUnits(result.returnData, 6); // USDC 6 位小數
        console.log(`查詢 #${i + 1}: ${balance} USDC`);
    });
}

optimizeCrossContractCalls().catch(console.error);

Python Multicall 實作:

from web3 import Web3
from eth_abi import encode

def multicall_balance():
    w3 = Web3(Web3.HTTPProvider("https://mainnet.infura.io/v3/YOUR_PROJECT_ID"))
    
    multicall_address = "0xcA11bde05977b3631167028862bE2a173976CA11"
    
    # Multicall3 合約 ABI
    multicall_abi = [{
        "inputs": [{
            "components": [
                {"name": "target", "type": "address"},
                {"name": "allowFailure", "type": "bool"},
                {"name": "callData", "type": "bytes"}
            ],
            "name": "calls",
            "type": "tuple[]"
        }],
        "name": "aggregate3",
        "outputs": [{
            "components": [
                {"name": "success", "type": "bool"},
                {"name": "returnData", "type": "bytes"}
            ],
            "name": "",
            "type": "tuple[]"
        }],
        "stateMutability": "payable",
        "type": "function"
    }]
    
    multicall = w3.eth.contract(address=multicall_address, abi=multicall_abi)
    
    # ERC20 代幣合約
    tokens = [
        "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48",  # USDC
        "0xdAC17F958D2ee523a2206206994597C13D831ec7",  # USDT
        "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2",  # WETH
    ]
    
    wallet_address = "0x742d35Cc6634C0532925a3b844Bc9e7595f5b5b0"
    
    # 建構批次呼叫
    calls = []
    for token_address in tokens:
        token_abi = [{
            "inputs": [{"name": "owner", "type": "address"}],
            "name": "balanceOf",
            "outputs": [{"type": "uint256"}],
            "stateMutability": "view",
            "type": "function"
        }]
        token = w3.eth.contract(address=token_address, abi=token_abi)
        
        # 編碼函數調用
        call_data = token.functions.balanceOf(wallet_address).build_transaction()['data']
        calls.append({
            'target': token_address,
            'allowFailure': False,
            'callData': call_data
        })
    
    # 執行批次呼叫
    result = multicall.functions.aggregate3(calls).call()
    
    print("批次查詢結果:")
    for i, token_address in enumerate(tokens):
        if result[i]['success']:
            balance = int.from_bytes(result[i]['returnData'], 'big')
            decimals = 6 if 'USDC' in str(tokens[i]) or 'USDT' in str(tokens[i]) else 18
            symbol = "USDC" if 'USDC' in str(tokens[i]) else ("USDT" if 'USDT' in str(tokens[i]) else "WETH")
            print(f"{symbol}: {balance / 10**decimals:.4f}")

multicall_balance()

Gas 退款機制

EIP-3529 修改了 Gas 退款規則:

退款上限 = (初始 Gas - 消耗的 Gas) / 2

這意味著你的退款永遠不會超過你消耗的一半。

contract RefundDemo {
    uint256 public counter;
    uint256[] public largeArray;
    
    constructor() {
        // 初始化一個大陣列(消耗 Gas)
        for (uint256 i = 0; i < 1000; i++) {
            largeArray.push(i);
        }
    }
    
    // 清除陣列(獲得大量退款)
    // 假設 initialGas = 100000
    // 清除消耗 = 2900 * 1000 = 2,900,000
    // 退還 = 10000 * 1000 = 10,000,000
    // 但退款上限 = (100000 - 2900000) / 2... 為負數
    // 實際退款 = 100000 / 2 = 50000
    function clearArray() external {
        delete largeArray;
    }
    
    // 更好的做法:分批清理
    function clearArrayBatched(uint256 batchSize) external {
        require(largeArray.length >= batchSize, "Not enough elements");
        
        for (uint256 i = 0; i < batchSize; i++) {
            largeArray.pop();
        }
        // 每次刪除都計算退款
    }
}

實用 Gas 估算工具

手動估算模式

// 完整 Gas 估算流程
const { ethers } = require("ethers");

async function fullGasEstimation() {
    const provider = new ethers.JsonRpcProvider("https://mainnet.infura.io/v3/YOUR_PROJECT_ID");
    
    const contractAddress = "YOUR_CONTRACT_ADDRESS";
    const abi = [
        "function myFunction(uint256[] calldata values) external returns (uint256)"
    ];
    
    const contract = new ethers.Contract(contractAddress, abi, provider);
    
    // 1. 估算執行 Gas
    const gasEstimate = await contract.myFunction.estimateGas([
        1, 2, 3, 4, 5, 6, 7, 8, 9, 10
    ]);
    console.log(`執行 Gas 估算: ${gasEstimate}`);
    
    // 2. 獲取當前費用數據
    const feeData = await provider.getFeeData();
    console.log(`基礎費用: ${feeData.baseFeePerGas ? ethers.formatUnits(feeData.baseFeePerGas, 'gwei') : 'N/A'} Gwei`);
    console.log(`優先費用: ${feeData.maxPriorityFeePerGas ? ethers.formatUnits(feeData.maxPriorityFeePerGas, 'gwei') : 'N/A'} Gwei`);
    
    // 3. 計算最大費用
    const maxFeePerGas = feeData.maxFeePerGas || feeData.gasPrice;
    const maxPriorityFeePerGas = feeData.maxPriorityFeePerGas || ethers.parseUnits("1", "gwei");
    
    // 4. 計算總費用
    const gasLimit = gasEstimate + 10000; // 加上 buffer
    const totalCost = gasLimit * maxFeePerGas;
    
    console.log(`建議 Gas 上限: ${gasLimit}`);
    console.log(`預估總費用: ${ethers.formatEther(totalCost)} ETH`);
    
    // 5. 估算礦工收入
    const minerReward = gasLimit * maxPriorityFeePerGas;
    console.log(`礦工小費: ${ethers.formatEther(minerReward)} ETH`);
}

fullGasEstimation().catch(console.error);

Python Gas 估算系統

from web3 import Web3
import time

class GasEstimator:
    def __init__(self, provider_url):
        self.w3 = Web3(Web3.HTTPProvider(provider_url))
        
    def estimate_transaction(self, contract_address, function_name, *args):
        """估算交易 Gas"""
        # 讀取代碼(用於估算)
        code = self.w3.eth.get_code(contract_address)
        print(f"合約位元組碼大小: {len(code)} bytes")
        
        # 估算 Gas
        # 這需要根據具體合約調整
        gas_estimate = 100000  # 預設值
        return gas_estimate
    
    def calculate_fee(self, gas_estimate):
        """計算費用"""
        fee_data = self.w3.eth.fee_history(1, 'latest')
        base_fee = int(fee_data['baseFeePerGas'][-1])
        priority_fee = int(self.w3.eth.max_priority_fee())
        
        max_fee = base_fee + priority_fee
        total_fee_wei = gas_estimate * max_fee
        
        return {
            'base_fee_gwei': base_fee / 10**9,
            'priority_fee_gwei': priority_fee / 10**9,
            'max_fee_gwei': max_fee / 10**9,
            'total_fee_eth': self.w3.from_wei(total_fee_wei, 'ether'),
            'total_fee_usd': self.w3.from_wei(total_fee_wei, 'ether') * 3500  # 假設 ETH = $3500
        }
    
    def recommend_gas_settings(self, urgency='normal'):
        """根據緊急性推薦 Gas 設定"""
        fee_data = self.w3.eth.fee_history(5, 'latest')
        avg_base_fee = sum(int(f) for f in fee_data['baseFeePerGas']) / len(fee_data['baseFeePerGas'])
        
        if urgency == 'fast':
            multiplier = 1.5
        elif urgency == 'slow':
            multiplier = 0.8
        else:
            multiplier = 1.1
            
        priority_fee = int(self.w3.eth.max_priority_fee())
        
        return {
            'base_fee_gwei': avg_base_fee / 10**9,
            'priority_fee_gwei': priority_fee / 10**9,
            'max_fee_gwei': int(avg_base_fee * multiplier + priority_fee) / 10**9
        }

# 使用範例
estimator = GasEstimator("https://mainnet.infura.io/v3/YOUR_PROJECT_ID")
settings = estimator.recommend_gas_settings('normal')
print(f"推薦 Gas 設定: {settings}")

Gas 最佳化實務技巧

技巧一:善用熱讀取優惠

// 不好的代碼
function bad(address user) external {
    if (balance[user] > 0 && balance[user] > minAmount) {
        // ...
    }
    // 第二次讀取 balance[user]
    if (balance[user] > 0) {
        // ...
    }
}

// 好的代碼
function good(address user) external {
    uint256 userBalance = balance[user]; // 一次讀取
    if (userBalance > 0 && userBalance > minAmount) {
        // ...
    }
    if (userBalance > 0) { // 使用快取
        // ...
    }
}

技巧二:使用 events 而不是回傳值

// 低效
function getValue() external view returns (uint256) {
    return storedValue; // SLOAD
}

// 高效
event ValueChanged(uint256 newValue);

function setValue(uint256 _value) external {
    storedValue = _value;
    emit ValueChanged(_value); // LOG 比 SSTORE 便宜
}

技巧三:避免在迴圈中進行 SLOAD

// 不好的代碼
function bad(address[] calldata users) external {
    for (uint256 i = 0; i < users.length; i++) {
        // 每個迴圈都 SLOAD
        if (balances[users[i]] > threshold) {
            // ...
        }
    }
}

// 好的代碼
function good(address[] calldata users) external {
    uint256 len = users.length;
    uint256[] memory cachedBalances = new uint256[](len);
    
    // 批量讀取
    for (uint256 i = 0; i < len; i++) {
        cachedBalances[i] = balances[users[i]];
    }
    
    // 使用快取
    for (uint256 i = 0; i < len; i++) {
        if (cachedBalances[i] > threshold) {
            // ...
        }
    }
}

結語:Gas 是以太坊的語法

搞懂了 Gas,你就搞懂了以太坊的一半。

這篇文章涵蓋了 Gas 計算的核心概念:基礎費用模型、Opcode 消耗、記憶體成本、SSTORE 機制、跨合約呼叫,以及實用的最佳化技巧。這些知識不只能幫你省 Gas,更重要的是讓你能寫出更高效的合約。

記住:每次合約執行都有代價,每筆狀態寫入都要付費。當你能用成本的角度思考合約設計,你就離優秀的以太坊工程師更近一步了。


資料截止日期:2026 年 3 月

一級來源

二級來源

工具

延伸閱讀與來源

這篇文章對您有幫助嗎?

評論

發表評論

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

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