以太坊錢包白名單設定與合約授權管理實作完整指南

以太坊錢包安全中最容易被忽視但又至關重要的環節是合約授權管理。本文深入探討以太坊錢包的授權機制、常見的安全風險、以及如何通過白名單設定和授權管理來保護資產安全。提供完整的實作程式碼,涵蓋從基礎的手動管理到自動化的智能合約監控系統,以及交易模擬器的使用指南。

以太坊錢包白名單設定與合約授權管理實作完整指南

概述

以太坊錢包安全中最容易被忽視但又至關重要的環節之一是「合約授權管理」(Token Approval)。當用戶與去中心化應用(DApp)交互時,往往需要授權該應用的合約使用你的代幣。然而,這種授權如果管理不當,可能成為駭客攻擊的突破口。

本文將深入探討以太坊錢包的授權機制、常見的安全風險、以及如何通過白名單設定和授權管理來保護資產安全。我們將提供完整的實作程式碼,涵蓋從基礎的手動管理到自動化的智能合約監控系統。

本指南特別適合已經開始使用 DeFi 應用但不了解授權風險的進階用戶,以及希望為用戶提供安全錢包功能的開發者。

第一章:以太坊代幣授權機制解析

1.1 ERC-20 授權機制原理

ERC-20 代幣標準定義了 approvetransferFrom 兩個函數,這是授權機制的核心:

ERC-20 授權機制流程:

┌─────────────────────────────────────────────────────┐
│                                                     │
│   用戶錢包                                          │
│   ├── 代幣餘額:1000 USDC                          │
│   └── 授權給 DApp:1000 USDC                       │
│              │                                      │
│              ▼                                      │
│   ┌─────────────────────────────────────┐         │
│   │        DApp 智能合約                 │         │
│   │                                     │         │
│   │  用戶授權後,合約可以代替用戶        │         │
│   │  調用 transferFrom 轉走代幣         │         │
│   │                                     │         │
│   └─────────────────────────────────────┘         │
│              │                                      │
│              ▼                                      │
│   轉移代幣完成                                      │
│   用戶餘額:0 USDC                                  │
│                                                     │
└─────────────────────────────────────────────────────┘

漏洞風險:
- 如果 DApp 合約存在漏洞,攻擊者可能盜用授權額度
- 無限授權(approve unlimited)讓風險最大化
- 取消授權需要發送新交易,有延遲風險

1.2 approve 函數的技術細節

// ERC-20 approve 函數標準定義

/**
 * @dev 授權給定地址使用指定數量的代幣
 * @param spender 有權使用代幣的地址
 * @param amount 可以使用的代幣數量
 */
function approve(address spender, uint256 amount) external returns (bool);

/**
 * @dev 返回 spender 被允許使用的代幣數量
 */
function allowance(address owner, address spender) external view returns (uint256);

常見的授權模式:

// 模式一:精確授權(推薦)
await token.approve(spender, exactAmount);
await dappContract.deposit(exactAmount);

// 模式二:設置上限授權
const currentAllowance = await token.allowance(user, spender);
const maxSafeAmount = await calculateSafeLimit(user);
if (currentAllowance < maxSafeAmount) {
  await token.approve(spender, maxSafeAmount);
}

// 模式三:無限授權(風險極高)
await token.approve(spender, ethers.MaxUint256);

1.3 授權安全的關鍵數據

授權風險關鍵指標:

┌─────────────────────────────────────────────────────┐
│                                                     │
│  1. 授權總量                                        │
│     所有授權的代幣總價值                            │
│     = Σ(授權金額 × 代幣價格)                       │
│                                                     │
│  2. 授權合約安全性                                  │
│     - 是否經過審計                                   │
│     - 合約是否可升級                               │
│     - 是否有緊急暫停機制                           │
│                                                     │
│  3. 歷史授權記錄                                    │
│     - 曾經授權過哪些合約                           │
│     - 是否仍需該授權                               │
│                                                     │
│  4. 授權年齡                                        │
│     - 長期未使用的授權可能是風險                   │
│                                                     │
└─────────────────────────────────────────────────────┘

安全建議:
- 定期審計授權清單
- 取消不再需要的授權
- 使用最小授權原則
- 監控異常授權活動

第二章:查詢和管理錢包授權

2.1 使用 Ethers.js 查詢授權

import { ethers } from 'ethers';

// 初始化 provider
const provider = new ethers.JsonRpcProvider('https://eth.llamarpc.com');

// ERC-20 代幣標準 ABI(僅授權相關函數)
const tokenApprovalABI = [
  'function allowance(address owner, address spender) view returns (uint256)',
  'function approve(address spender, uint256 amount) returns (bool)',
  'function balanceOf(address account) view returns (uint256)',
  'event Approval(address indexed owner, address indexed spender, uint256 value)',
];

// 查詢單筆授權
const checkAllowance = async (tokenAddress, owner, spender) => {
  const token = new ethers.Contract(tokenAddress, tokenApprovalABI, provider);
  const allowance = await token.allowance(owner, spender);
  return allowance;
};

// 批量查詢授權
const batchCheckAllowances = async (tokenAddress, owner, spenders) => {
  const results = [];
  
  for (const spender of spenders) {
    const allowance = await checkAllowance(tokenAddress, owner, spender);
    results.push({
      spender,
      allowance: allowance.toString(),
      allowanceFormatted: ethers.formatUnits(allowance, 18),
      hasApproval: allowance > 0n,
    });
  }
  
  return results;
};

// 查詢某錢包的所有授權事件
const getApprovalHistory = async (address, fromBlock = 0) => {
  const tokenABI = [
    'event Approval(address indexed owner, address indexed spender, uint256 value)',
  ];
  
  // 查詢主要代幣
  const tokens = [
    '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48', // USDC
    '0xdAC17F958D2ee523a2206206994597C13D831ec7',  // USDT
    '0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2',  // WETH
    '0x2260FAC5E5542a773Aa44fBCfeDf7C193bc2C599',  // WBTC
  ];
  
  const approvals = [];
  
  for (const token of tokens) {
    const tokenContract = new ethers.Contract(token, tokenABI, provider);
    
    const filter = tokenContract.filters.Approval(address);
    const events = await tokenContract.queryFilter(filter, fromBlock, 'latest');
    
    for (const event of events) {
      if (event.args[0].toLowerCase() === address.toLowerCase()) {
        approvals.push({
          token: token,
          spender: event.args[1],
          amount: event.args[2].toString(),
          blockNumber: event.blockNumber,
          transactionHash: event.transactionHash,
          timestamp: (await event.getBlock()).timestamp,
        });
      }
    }
  }
  
  return approvals.sort((a, b) => b.timestamp - a.timestamp);
};

2.2 使用 The Graph 查詢授權

// 部署自己的 Subgraph 或使用現有服務
// 以下是以 UnspentApproval 查詢為例的 GraphQL 查詢

const GRAPHQL_QUERY = `
  query GetTokenApprovals($owner: String!) {
    tokenTransfers(
      where: { from: $owner }
    ) {
      id
      from
      to
      value
      token {
        id
        symbol
        decimals
      }
    }
    
    approvals(
      where: { owner: $owner }
    ) {
      id
      owner
      spender
      value
      token {
        id
        symbol
      }
      transaction {
        timestamp
      }
    }
  }
`;

// 使用 GraphQL 端點
const fetchFromGraph = async (endpoint, query, variables) => {
  const response = await fetch(endpoint, {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ query, variables }),
  });
  
  return response.json();
};

// 示例查詢
const getMyApprovals = async (address) => {
  // 使用 DeBank 或其他 API
  const DEBANK_API = 'https://api.debank.com/v1';
  
  const response = await fetch(`${DEBANK_API}/token_approval_list`, {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
      'Authorization': `Bearer ${DEBANK_API_KEY}`,
    },
    body: JSON.stringify({
      user_addr: address,
      chain_id: 1, // Ethereum Mainnet
    }),
  });
  
  return response.json();
};

2.3 授權管理工具

以下是主流的授權管理工具:

授權管理工具比較:

┌────────────────┬──────────┬──────────┬──────────────────┐
│     工具       │   免費   │  風險評估 │    批量管理      │
├────────────────┼──────────┼──────────┼──────────────────┤
│ revoke.cash    │   是     │   是     │     是           │
├────────────────┼──────────┼──────────┼──────────────────┤
│ approved.zone  │   是     │   是     │     是           │
├────────────────┼──────────┼──────────┼──────────────────┤
│ Etherscan      │   是     │   部分    │     是           │
├────────────────┼──────────┼──────────┼──────────────────┤
│ DeBank         │   是     │   是     │     是           │
├────────────────┼──────────┼──────────┼──────────────────┤
│ Zerion          │   是     │   是     │     是           │
├────────────────┼──────────┼──────────┼──────────────────┤
│ Rabby          │   是     │   是     │     部分          │
└────────────────┴──────────┴──────────┴──────────────────┘

第三章:智能合約白名單實現

3.1 錢包白名單合約設計

對於高安全性需求的用戶,可以部署自己的白名單合約來控制代幣轉移:

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

import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol";

/// @title 代幣白名單合約
/// @notice 用戶將代幣存入此合約,只有白名單地址可以調用 transferFrom
contract TokenWhitelist is Ownable {
    using SafeERC20 for IERC20;
    
    // --- 狀態變數 ---
    
    /// @notice 錢包所有者
    address public immutable wallet;
    
    /// @notice 白名單地址映射
    mapping(address => bool) public whitelist;
    
    /// @notice 每日轉帳限額
    mapping(address => uint256) public dailyLimit;
    mapping(address => uint256) public dailyLimitUsed;
    mapping(address => uint256) public lastResetTime;
    
    /// @notice 每筆交易限額
    mapping(address => uint256) public perTxLimit;
    
    /// @notice 合約啟用狀態
    bool public isActive = true;
    
    // --- 事件 ---
    
    event AddedToWhitelist(address indexed account);
    event RemovedFromWhitelist(address indexed account);
    event WhitelistDisabled(address indexed owner);
    event TransferApproved(
        address indexed token,
        address indexed to,
        uint256 amount
    );
    event DailyLimitUpdated(address indexed token, uint256 newLimit);
    
    // --- 修改器 ---
    
    modifier onlyWhitelisted() {
        require(whitelist[msg.sender], "Caller not whitelisted");
        _;
    }
    
    modifier whenActive() {
        require(isActive, "Contract is paused");
        _;
    }
    
    // --- 初始化 ---
    
    constructor(address _wallet) Ownable() {
        require(_wallet != address(0), "Zero wallet address");
        wallet = _wallet;
    }
    
    // --- 白名單管理 ---
    
    /// @notice 添加到白名單
    function addToWhitelist(address account) external onlyOwner {
        require(account != address(0), "Invalid address");
        require(!whitelist[account], "Already whitelisted");
        
        whitelist[account] = true;
        emit AddedToWhitelist(account);
    }
    
    /// @notice 批量添加到白名單
    function batchAddToWhitelist(address[] calldata accounts) external onlyOwner {
        for (uint256 i = 0; i < accounts.length; i++) {
            require(accounts[i] != address(0), "Invalid address");
            whitelist[accounts[i]] = true;
            emit AddedToWhitelist(accounts[i]);
        }
    }
    
    /// @notice 從白名單移除
    function removeFromWhitelist(address account) external onlyOwner {
        require(whitelist[account], "Not in whitelist");
        whitelist[account] = false;
        emit RemovedFromWhitelist(account);
    }
    
    /// @notice 檢查地址是否在白名單中
    function isWhitelisted(address account) external view returns (bool) {
        return whitelist[account];
    }
    
    // --- 轉帳控制 ---
    
    /// @notice 執行轉帳(僅白名單地址可調用)
    /// @dev 錢包所有者發起交易,但由白名單合約執行實際轉帳
    function executeTransfer(
        address token,
        address to,
        uint256 amount
    ) external onlyWhitelisted whenActive returns (bool) {
        // 檢查單筆限額
        if (perTxLimit[token] > 0) {
            require(amount <= perTxLimit[token], "Exceeds per-tx limit");
        }
        
        // 檢查每日限額
        if (dailyLimit[token] > 0) {
            _resetDailyUsageIfNeeded(token);
            require(
                dailyLimitUsed[token] + amount <= dailyLimit[token],
                "Exceeds daily limit"
            );
            dailyLimitUsed[token] += amount;
        }
        
        // 執行轉帳
        IERC20(token).safeTransfer(to, amount);
        emit TransferApproved(token, to, amount);
        
        return true;
    }
    
    /// @notice 執行從合約到目標地址的 transferFrom
    function executeTransferFrom(
        address token,
        address from,
        address to,
        uint256 amount
    ) external onlyWhitelisted whenActive returns (bool) {
        if (perTxLimit[token] > 0) {
            require(amount <= perTxLimit[token], "Exceeds per-tx limit");
        }
        
        if (dailyLimit[token] > 0) {
            _resetDailyUsageIfNeeded(token);
            require(
                dailyLimitUsed[token] + amount <= dailyLimit[token],
                "Exceeds daily limit"
            );
            dailyLimitUsed[token] += amount;
        }
        
        IERC20(token).safeTransferFrom(from, to, amount);
        emit TransferApproved(token, to, amount);
        
        return true;
    }
    
    // --- 限額管理 ---
    
    /// @notice 設置每日轉帳限額
    function setDailyLimit(address token, uint256 limit) external onlyOwner {
        dailyLimit[token] = limit;
        emit DailyLimitUpdated(token, limit);
    }
    
    /// @notice 設置單筆交易限額
    function setPerTxLimit(address token, uint256 limit) external onlyOwner {
        perTxLimit[token] = limit;
    }
    
    /// @notice 重置每日使用量
    function resetDailyUsage(address token) external onlyOwner {
        dailyLimitUsed[token] = 0;
        lastResetTime[token] = block.timestamp;
    }
    
    /// @notice 查看剩餘每日額度
    function getRemainingDailyLimit(address token) external view returns (uint256) {
        if (dailyLimit[token] == 0) {
            return type(uint256).max;
        }
        
        uint256 lastReset = lastResetTime[token];
        if (block.timestamp - lastReset >= 86400) {
            return dailyLimit[token];
        }
        
        return dailyLimit[token] - dailyLimitUsed[token];
    }
    
    // --- 緊急功能 ---
    
    /// @notice 禁用白名單(緊急解鎖)
    function disableWhitelist() external onlyOwner {
        isActive = false;
        emit WhitelistDisabled(msg.sender);
    }
    
    /// @notice 提取所有代幣(緊急)
    function emergencyWithdraw(
        address token,
        address to,
        uint256 amount
    ) external onlyOwner {
        IERC20(token).safeTransfer(to, amount);
    }
    
    // --- 私有函數 ---
    
    function _resetDailyUsageIfNeeded(address token) private {
        if (block.timestamp - lastResetTime[token] >= 86400) {
            dailyLimitUsed[token] = 0;
            lastResetTime[token] = block.timestamp;
        }
    }
}

3.2 限制代理合約(Limit Order Contract)

創建一個受限的代幣管理合約,限制只能轉帳到白名單地址:

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

import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";

/// @title 安全轉帳代理合約
/// @notice 限制只能將代幣轉帳到白名單地址
contract SafeTransferProxy {
    using SafeERC20 for IERC20;
    
    // --- 狀態 ---
    
    /// @notice 所有者地址
    address public owner;
    
    /// @notice 白名單地址集合
    mapping(address => bool) public whitelist;
    
    /// @notice 每個代幣的每日限額
    mapping(address => uint256) public dailyLimit;
    mapping(address => uint256) public dailyUsed;
    mapping(address => uint256) public lastReset;
    
    // --- 事件 ---
    
    event WhitelistUpdated(address indexed target, bool status);
    event TransferCompleted(
        address indexed token,
        address indexed to,
        uint256 amount
    );
    event DailyLimitSet(address indexed token, uint256 limit);
    
    // --- 修改器 ---
    
    modifier onlyOwner() {
        require(msg.sender == owner, "Not owner");
        _;
    }
    
    // --- 初始化 ---
    
    constructor() {
        owner = msg.sender;
    }
    
    // --- 白名單管理 ---
    
    function addToWhitelist(address target) external onlyOwner {
        whitelist[target] = true;
        emit WhitelistUpdated(target, true);
    }
    
    function removeFromWhitelist(address target) external onlyOwner {
        whitelist[target] = false;
        emit WhitelistUpdated(target, false);
    }
    
    function isWhitelisted(address target) external view returns (bool) {
        return whitelist[target];
    }
    
    // --- 限額管理 ---
    
    function setDailyLimit(address token, uint256 limit) external onlyOwner {
        dailyLimit[token] = limit;
        emit DailyLimitSet(token, limit);
    }
    
    function getRemainingDailyLimit(address token) external view returns (uint256) {
        if (dailyLimit[token] == 0) {
            return type(uint256).max;
        }
        
        if (block.timestamp - lastReset[token] >= 86400) {
            return dailyLimit[token];
        }
        
        return dailyLimit[token] - dailyUsed[token];
    }
    
    // --- 核心轉帳功能 ---
    
    /// @notice 安全轉帳(僅限白名單)
    function safeTransfer(
        address token,
        address to,
        uint256 amount
    ) external onlyOwner returns (bool) {
        require(whitelist[to], "Recipient not whitelisted");
        
        // 檢查限額
        if (dailyLimit[token] > 0) {
            if (block.timestamp - lastReset[token] >= 86400) {
                dailyUsed[token] = 0;
                lastReset[token] = block.timestamp;
            }
            
            require(
                dailyUsed[token] + amount <= dailyLimit[token],
                "Daily limit exceeded"
            );
            dailyUsed[token] += amount;
        }
        
        IERC20(token).safeTransfer(to, amount);
        emit TransferCompleted(token, to, amount);
        
        return true;
    }
    
    /// @notice 批量轉帳
    function batchTransfer(
        address[] calldata tokens,
        address[] calldata recipients,
        uint256[] calldata amounts
    ) external onlyOwner returns (bool) {
        require(
            tokens.length == recipients.length &&
            recipients.length == amounts.length,
            "Length mismatch"
        );
        
        for (uint256 i = 0; i < tokens.length; i++) {
            safeTransfer(tokens[i], recipients[i], amounts[i]);
        }
        
        return true;
    }
}

第四章:白名單錢包前端實現

4.1 React + Ethers.js 實現

import React, { useState, useEffect } from 'react';
import { ethers } from 'ethers';

// 授權合約 ABI
const APPROVAL_ABI = [
  'function approve(address spender, uint256 amount) returns (bool)',
  'function allowance(address owner, address spender) view returns (uint256)',
  'event Approval(address indexed owner, address indexed spender, uint256 value)',
];

// 已知的 DApp 合約白名單
const KNOWN_DAPPS = {
  '0x7a250d5630B4cF539739dF2C5dAcb4c659F2488D': { name: 'Uniswap V2 Router', risk: 'low' },
  '0x68b3465833fb72A70ecDF485E0e4C7bD8665Fc45': { name: 'Uniswap V3 Router', risk: 'low' },
  '0xEf1c6E67703c7BD7107eed8303Fbe6EC2554BF6B': { name: 'Uniswap V3 Positions NFT', risk: 'medium' },
  '0x3d9819210A31b4961b30EF54bE2aeD79B9c9Cd3B': { name: 'Compound Comptroller', risk: 'low' },
  '0x87870Bca3F3fD6335C3F4cE2E13E25FFa541Cb4d': { name: 'Aave V3 Pool', risk: 'low' },
  '0xBA12222222228d8Ba445958a75a0704d566BF2C8': { name: 'Balancer Vault', risk: 'low' },
};

// 風險評估函數
const assessRisk = (spenderAddress: string): { level: string; message: string } => {
  const knownDapp = KNOWN_DAPPS[spenderAddress.toLowerCase()];
  
  if (knownDapp) {
    return {
      level: knownDapp.risk,
      message: `已知合約:${knownDapp.name}`,
    };
  }
  
  // 未知合約默認為高風險
  return {
    level: 'high',
    message: '未知合約,請確認後再授權',
  };
};

interface ApprovalManagerProps {
  walletAddress: string;
  provider: ethers.BrowserProvider;
}

const ApprovalManager: React.FC<ApprovalManagerProps> = ({ walletAddress, provider }) => {
  const [approvals, setApprovals] = useState<Approval[]>([]);
  const [loading, setLoading] = useState(true);
  const [selectedApprovals, setSelectedApprovals] = useState<Set<string>>(new Set());
  
  // 獲取錢包的所有授權
  const fetchApprovals = async () => {
    setLoading(true);
    
    try {
      // 使用 Etherscan API 獲取授權事件
      const response = await fetch(
        `https://api.etherscan.io/api?module=account&action=tokentx&address=${walletAddress}&sort=desc&apikey=${ETHERSCAN_API_KEY}`
      );
      
      const data = await response.json();
      const approvals = data.result
        .filter((tx: any) => tx.method === 'approve')
        .map((tx: any) => ({
          token: tx.tokenSymbol,
          spender: tx.to,
          amount: tx.value,
          hash: tx.hash,
          time: new Date(tx.timeStamp * 1000),
          risk: assessRisk(tx.to),
        }));
      
      setApprovals(approvals);
    } catch (error) {
      console.error('Failed to fetch approvals:', error);
    } finally {
      setLoading(false);
    }
  };
  
  // 撤銷授權
  const revokeApproval = async (tokenAddress: string, spenderAddress: string) => {
    if (!window.ethereum) {
      throw new Error('Please install MetaMask');
    }
    
    const signer = await provider.getSigner();
    const token = new ethers.Contract(tokenAddress, APPROVAL_ABI, signer);
    
    // 將授權設為 0
    const tx = await token.approve(spenderAddress, 0);
    console.log('Revoking approval:', tx.hash);
    
    await tx.wait();
    console.log('Approval revoked');
    
    // 刷新列表
    await fetchApprovals();
  };
  
  // 批量撤銷高風險授權
  const revokeHighRiskApprovals = async () => {
    const highRiskApprovals = approvals.filter(
      (a) => a.risk.level === 'high' && !a.amount.eq(0)
    );
    
    for (const approval of highRiskApprovals) {
      try {
        await revokeApproval(approval.token, approval.spender);
      } catch (error) {
        console.error(`Failed to revoke ${approval.spender}:`, error);
      }
    }
  };
  
  // 設置精確授權
  const setPreciseApproval = async (
    tokenAddress: string,
    spenderAddress: string,
    amount: bigint
  ) => {
    const signer = await provider.getSigner();
    const token = new ethers.Contract(tokenAddress, APPROVAL_ABI, signer);
    
    const tx = await token.approve(spenderAddress, amount);
    console.log('Setting approval:', tx.hash);
    
    await tx.wait();
    console.log('Approval updated');
    
    await fetchApprovals();
  };
  
  return (
    <div className="approval-manager">
      <h2>代幣授權管理</h2>
      
      <div className="actions">
        <button onClick={fetchApprovals} disabled={loading}>
          {loading ? '載入中...' : '刷新'}
        </button>
        <button
          onClick={revokeHighRiskApprovals}
          className="danger"
        >
          撤銷高風險授權
        </button>
      </div>
      
      <table>
        <thead>
          <tr>
            <th>代幣</th>
            <th>授權目標</th>
            <th>數量</th>
            <th>風險</th>
            <th>時間</th>
            <th>操作</th>
          </tr>
        </thead>
        <tbody>
          {approvals.map((approval) => (
            <tr key={approval.hash}>
              <td>{approval.token}</td>
              <td>
                <span className={`risk-badge ${approval.risk.level}`}>
                  {approval.risk.message}
                </span>
              </td>
              <td>
                {approval.amount.eq(ethers.MaxUint256)
                  ? '無限'
                  : ethers.formatUnits(approval.amount, 18)}
              </td>
              <td>
                <span className={`risk-indicator ${approval.risk.level}`}>
                  {approval.risk.level.toUpperCase()}
                </span>
              </td>
              <td>{approval.time.toLocaleDateString()}</td>
              <td>
                {approval.risk.level === 'high' && (
                  <button onClick={() => revokeApproval(approval.token, approval.spender)}>
                    撤銷
                  </button>
                )}
              </td>
            </tr>
          ))}
        </tbody>
      </table>
    </div>
  );
};

4.2 自動化監控腳本

// 授權監控機器人

const ethers = require('ethers');

class ApprovalMonitor {
  constructor(privateKey, rpcUrl, alertConfig) {
    this.wallet = new ethers.Wallet(privateKey, new ethers.JsonRpcProvider(rpcUrl));
    this.alertConfig = alertConfig;
    this.lastCheck = 0;
  }
  
  // 檢查錢包的授權狀態
  async checkApprovals(walletAddress) {
    const provider = this.wallet.provider;
    
    // 主要代幣列表
    const tokens = [
      { address: '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48', symbol: 'USDC', decimals: 6 },
      { address: '0xdAC17F958D2ee523a2206206994597C13D831ec7', symbol: 'USDT', decimals: 6 },
      { address: '0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2', symbol: 'WETH', decimals: 18 },
    ];
    
    const results = [];
    
    for (const token of tokens) {
      const tokenContract = new ethers.Contract(token.address, [
        'function allowance(address, address) view returns (uint256)',
        'function balanceOf(address) view returns (uint256)',
      ], provider);
      
      // 檢查無限授權(這是高風險信號)
      const maxUint = ethers.MaxUint256;
      
      // 檢查常見 DApp
      const dapps = [
        '0x7a250d5630B4cF539739dF2C5dAcb4c659F2488D', // Uniswap V2
        '0x68b3465833fb72A70ecDF485E0e4C7bD8665Fc45', // Uniswap V3
      ];
      
      for (const dapp of dapps) {
        const allowance = await tokenContract.allowance(walletAddress, dapp);
        
        if (allowance > 0) {
          results.push({
            token: token.symbol,
            spender: dapp,
            allowance: allowance.toString(),
            isUnlimited: allowance === maxUint,
            isHighRisk: allowance === maxUint,
          });
        }
      }
    }
    
    return results;
  }
  
  // 批量撤銷授權
  async revokeAllApprovals(walletAddress, tokens, spender) {
    const tokenABI = [
      'function approve(address spender, uint256 amount) returns (bool)',
    ];
    
    const results = [];
    
    for (const token of tokens) {
      const tokenContract = new ethers.Contract(token.address, tokenABI, this.wallet);
      
      try {
        const tx = await tokenContract.approve(spender, 0);
        console.log(`Revoking ${token.symbol} approval: ${tx.hash}`);
        await tx.wait();
        results.push({ token: token.symbol, success: true });
      } catch (error) {
        console.error(`Failed to revoke ${token.symbol}:`, error);
        results.push({ token: token.symbol, success: false, error: error.message });
      }
    }
    
    return results;
  }
  
  // 設置安全限額授權
  async setSafeApproval(tokenAddress, spender, limit, isDaily = false) {
    const tokenContract = new ethers.Contract(tokenAddress, [
      'function approve(address spender, uint256 amount) returns (bool)',
    ], this.wallet);
    
    const currentAllowance = await tokenContract.allowance(
      this.wallet.address,
      spender
    );
    
    if (currentAllowance >= limit) {
      console.log('Current allowance is sufficient');
      return;
    }
    
    const tx = await tokenContract.approve(spender, limit);
    console.log(`Setting approval to ${limit}: ${tx.hash}`);
    
    await tx.wait();
    console.log('Approval set successfully');
  }
  
  // 啟動監控
  startMonitoring(intervalMs = 60000) {
    const monitor = async () => {
      try {
        const approvals = await this.checkApprovals(this.wallet.address);
        
        // 檢查高風險授權
        const highRiskApprovals = approvals.filter(a => a.isHighRisk);
        
        if (highRiskApprovals.length > 0) {
          await this.sendAlert('HIGH_RISK', highRiskApprovals);
        }
        
        this.lastCheck = Date.now();
      } catch (error) {
        console.error('Monitoring error:', error);
      }
    };
    
    this.intervalId = setInterval(monitor, intervalMs);
    monitor(); // 立即執行一次
    
    return () => clearInterval(this.intervalId);
  }
  
  // 發送警告
  async sendAlert(type, data) {
    console.log(`[ALERT ${type}]:`, data);
    
    if (this.alertConfig?.telegram) {
      // 發送 Telegram 通知
      await fetch(`https://api.telegram.org/${this.alertConfig.telegram}/sendMessage`, {
        method: 'POST',
        body: JSON.stringify({
          chat_id: this.alertConfig.chatId,
          text: `⚠️ 授權風險警告\n\n${JSON.stringify(data, null, 2)}`,
        }),
      });
    }
  }
}

// 使用範例
const monitor = new ApprovalMonitor(
  process.env.PRIVATE_KEY,
  'https://eth.llamarpc.com',
  {
    telegram: process.env.TELEGRAM_BOT_TOKEN,
    chatId: process.env.TELEGRAM_CHAT_ID,
  }
);

// 啟動監控
monitor.startMonitoring(30000); // 每 30 秒檢查一次

// 或者執行一次性檢查
(async () => {
  const approvals = await monitor.checkApprovals('0x1234...');
  console.log('Current approvals:', approvals);
  
  // 如果有高風險授權,自動撤銷
  const highRisk = approvals.filter(a => a.isUnlimited);
  if (highRisk.length > 0) {
    console.log('Found unlimited approvals, revoking...');
    await monitor.revokeAllApprovals(
      '0x1234...',
      [
        { address: '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48', symbol: 'USDC' },
      ],
      '0x7a250d5630B4cF539739dF2C5dAcb4c659F2488D'
    );
  }
})();

第五章:交易模擬器使用指南

5.1 交易模擬的重要性

在執行任何重要的區塊鏈交易之前,進行交易模擬是避免資金損失的關鍵步驟:

交易模擬使用場景:

┌─────────────────────────────────────────────────────┐
│                                                     │
│  1. 代幣授權前                                      │
│     - 模擬授權後的代幣流向                          │
│     - 確認合約地址正確                             │
│     - 預估最大損失                                 │
│                                                     │
│  2. 複雜的 DeFi 操作                                │
│     - DEX 交易                                      │
│     - 借貸操作                                     │
│     - 流動性操作                                   │
│                                                     │
│  3. 批量交易                                        │
│     - 驗證批量操作的結果                           │
│     - 預估 Gas 費用                                │
│                                                     │
│  4. 緊急操作                                        │
│     - 清算保護                                     │
│     - 緊急提款                                     │
│                                                     │
└─────────────────────────────────────────────────────┘

5.2 Tenderly 交易模擬

Tenderly 是目前最流行的交易模擬平台:

import axios from 'axios';

class TenderlySimulator {
  constructor(accessKey, accountSlug, projectSlug) {
    this.accessKey = accessKey;
    this.accountSlug = accountSlug;
    this.projectSlug = projectSlug;
    this.baseUrl = `https://api.tenderly.co/api/v1/account/${accountSlug}/project/${projectSlug}`;
  }
  
  // 模擬交易
  async simulate(tx) {
    const response = await axios.post(
      `${this.baseUrl}/simulate`,
      {
        network_id: '1', // Mainnet
        from: tx.from,
        to: tx.to,
        input: tx.data,
        value: tx.value || '0x0',
        gas: tx.gas || 8000000,
        gas_price: tx.gasPrice || '0x0',
        save: true,
        save_if_fails: true,
      },
      {
        headers: {
          'X-Access-Key': this.accessKey,
          'Content-Type': 'application/json',
        },
      }
    );
    
    return {
      success: response.data.simulation.status === 'success',
      gasUsed: response.data.simulation.gas_used,
      returnValue: response.data.simulation.return_value,
      logs: response.data.simulation.logs,
      error: response.data.simulation.error,
    };
  }
  
  // 批量模擬(攻擊測試)
  async simulateAttack(attackTx, victimState) {
    const response = await axios.post(
      `${this.baseUrl}/simulate-bundle`,
      {
        network_id: '1',
        simulations: [
          {
            // 設置受害者狀態
            state_overrides: victimState,
          },
          {
            // 攻擊交易
            ...attackTx,
          },
        ],
      },
      {
        headers: {
          'X-Access-Key': this.accessKey,
          'Content-Type': 'application/json',
        },
      }
    );
    
    return response.data;
  }
}

// 使用示例
const simulator = new TenderlySimulator(
  process.env.TENDERLY_ACCESS_KEY,
  'your-account',
  'your-project'
);

// 模擬代幣授權
const approvalSimulation = await simulator.simulate({
  from: '0xWalletAddress',
  to: '0xTokenAddress',
  data: token.interface.encodeFunctionData('approve', [
    '0xDappAddress',
    ethers.MaxUint256,
  ]),
});

console.log('Approval simulation result:', approvalSimulation);

5.3 防火牆模式交易模擬

對於錢包應用,可以實現自己的交易模擬防火牆:

class TransactionFirewall {
  constructor(provider) {
    this.provider = provider;
    this.rules = [];
  }
  
  // 添加規則
  addRule(rule) {
    this.rules.push(rule);
  }
  
  // 評估交易
  async evaluate(tx) {
    const results = [];
    
    for (const rule of this.rules) {
      const result = await rule.evaluate(tx, this.provider);
      results.push({
        rule: rule.name,
        passed: result.passed,
        message: result.message,
        severity: result.severity,
      });
    }
    
    return results;
  }
  
  // 模擬執行(只讀)
  async simulate(tx) {
    try {
      // 使用 eth_call 進行只讀模擬
      const result = await this.provider.call({
        from: tx.from,
        to: tx.to,
        data: tx.data,
        value: tx.value,
        blockNumber: 'latest',
      });
      
      return {
        success: true,
        returnData: result,
      };
    } catch (error) {
      return {
        success: false,
        error: error.message,
      };
    }
  }
  
  // 估計 Gas
  async estimateGas(tx) {
    try {
      const gas = await this.provider.estimateGas(tx);
      return { success: true, gas };
    } catch (error) {
      return { success: false, error: error.message };
    }
  }
}

// 預設規則工廠
class RuleFactory {
  // 地址白名單規則
  static createWhitelistRule(whitelist) {
    return {
      name: 'Whitelist',
      evaluate: async (tx) => {
        const isWhitelisted = whitelist.includes(tx.to?.toLowerCase());
        return {
          passed: isWhitelisted,
          message: isWhitelisted
            ? 'Contract is whitelisted'
            : 'Contract not in whitelist',
          severity: isWhitelisted ? 'info' : 'warning',
        };
      },
    };
  }
  
  // 授權限額規則
  static createApprovalLimitRule(maxAmount) {
    return {
      name: 'ApprovalLimit',
      evaluate: async (tx, provider) => {
        // 如果是 approve 調用
        if (tx.data.startsWith('0x095ea7b3')) {
          const decoded = ethers.utils.defaultAbiCoder.decode(
            ['address', 'uint256'],
            '0x' + tx.data.slice(10)
          );
          const amount = decoded[1];
          
          const exceedsLimit = amount > maxAmount;
          return {
            passed: !exceedsLimit,
            message: exceedsLimit
              ? `Approval amount ${amount} exceeds limit ${maxAmount}`
              : 'Approval amount is within limit',
            severity: exceedsLimit ? 'warning' : 'info',
          };
        }
        
        return { passed: true, message: 'Not an approval call', severity: 'info' };
      },
    };
  }
  
  // 合約風險評估規則
  static createContractRiskRule() {
    const knownRiskyContracts = new Set([
      // 已知的高風險合約
    ]);
    
    return {
      name: 'ContractRisk',
      evaluate: async (tx) => {
        const isRisky = knownRiskyContracts.has(tx.to?.toLowerCase());
        return {
          passed: !isRisky,
          message: isRisky
            ? 'This contract has known risks'
            : 'No known risks detected',
          severity: isRisky ? 'critical' : 'info',
        };
      },
    };
  }
}

// 使用示例
const firewall = new TransactionFirewall(provider);

// 添加規則
firewall.addRule(RuleFactory.createWhitelistRule([
  '0x7a250d5630B4cF539739dF2C5dAcb4c659F2488D', // Uniswap
  '0x87870Bca3F3fD6335C3F4cE2E13E25FFa541Cb4d', // Aave
]));

firewall.addRule(RuleFactory.createApprovalLimitRule(
  ethers.parseUnits('1000', 6) // USDC 最多授權 1000
));

firewall.addRule(RuleFactory.createContractRiskRule());

// 評估交易
const evaluation = await firewall.evaluate(tx);

// 如果有嚴重問題,阻止交易
const hasBlockingIssues = evaluation.some(
  r => r.severity === 'critical' && !r.passed
);

if (hasBlockingIssues) {
  throw new Error('Transaction blocked by firewall');
}

結論:最佳實踐建議

白名單設定最佳實踐

  1. 最小授權原則:只授權必要的金額,避免「無限授權」
  2. 定期審計:每月審計一次錢包的所有授權
  3. 風險分類:將常用 DApp 加入白名單,未知合約設為高風險
  4. 限額保護:對重要資產設置每日和單筆轉帳限額
  5. 自動化監控:部署監控腳本,及時發現異常授權

合約授權管理清單

授權管理檢查清單:

□ 授權前
  □ 確認合約地址正確
  □ 使用交易模擬器測試
  □ 設置合理的授權額度
  □ 記錄授權歷史

□ 授權後
  □ 監控授權使用情況
  □ 檢查錢包餘額變化
  □ 保留授權記錄

□ 取消授權
  □ 不再使用的 DApp 及時取消授權
  □ 取消授權後再次確認
  □ 記錄取消操作

□ 定期審計
  □ 每月檢查一次所有授權
  □ 識別並撤銷高風險授權
  □ 更新白名單規則

通過實施本文介紹的白名單設定和授權管理策略,用戶可以顯著降低因合約漏洞或惡意攻擊導致的資產損失風險。安全無小事,每一次授權都應該經過深思熟慮。

重要參考資源

延伸閱讀與來源

這篇文章對您有幫助嗎?

評論

發表評論

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

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