Safe{Wallet} 智慧合約錢包完整開發者指南:從部署到自定義模組的工程實踐

Safe(原 Gnosis Safe)是以太坊生態系統中最廣泛使用的智慧合約錢包解決方案。本文從工程師視角出發,提供 Safe 錢包的完整開發者指南,涵蓋錢包部署、Safe Core SDK 使用、Ownable Plugins 開發、Keeper 任務系統、以及自定義模組創建等核心主題。透過完整的程式碼範例和部署腳本,開發者可以快速掌握 Safe 開發的核心技術。

Safe{Wallet} 智慧合約錢包完整開發者指南:從部署到自定義模組的工程實踐

執行摘要

Safe(原 Gnosis Safe)是以太坊生態系統中最廣泛使用的智慧合約錢包解決方案,截至 2026 年第一季度,已保護超過 1000 億美元的數位資產。Safe 的核心優勢在於其模組化架構、多重簽名機制、以及對 ERC-4337 帳戶抽象標準的原生支持。本文從工程師視角出發,提供 Safe 錢包的完整開發者指南,涵蓋錢包部署、Safe Core SDK 使用、Ownable Plugins 開發、 keeper 任務系統、以及自定義模組創建等核心主題。

本文假設讀者具備 Solidity 智慧合約開發基礎,熟悉以太坊開發工具鏈(Hardhat/Foundry),並對帳戶抽象概念有基本理解。我們將提供完整的程式碼範例、部署腳本、以及常見問題的解決方案。

第一章:Safe 錢包架構深度解析

1.1 Safe 核心合約架構

Safe 錢包採用高度模組化的合約架構設計,這種設計使得 Safe 能夠在不修改核心錢包邏輯的情況下,支援各種擴展功能。理解 Safe 的合約架構是進行 Safe 開發的基礎。

SafeProxyFactory 是負責創建 Safe 錢包實例的工廠合約。Safe 使用代理模式(Proxy Pattern)來實現可升級性:當 Safe 部署新版本時,只需要部署新的實作合約,而所有現有的 Safe 錢包可以通過指向新的實作來獲得功能更新,而無需重新創建錢包。SafeProxyFactory 的核心方法是 createProxyWithNonce 和 createProxyWithScript,前者使用簡單的 nonce 機制,後者支援更複雜的初始化腳本。

Safe 合約本身是錢包的實作邏輯所在。Safe 合約繼承自多個基礎合約:Initializable 提供了初始化機制;ModuleManager 處理模組的管理(添加、移除模組);GuardManager 處理 Guard 的管理;SignatureDecoder 處理簽名解碼。每個 Safe 錢包實例都有一個 owners 列表(一組以太坊地址)和一個 threshold 參數(執行交易所需的最小簽名數量)。

Safe.sol 的核心資料結構包括:owners 是 address[] 類型,儲存所有所有者的地址;threshold 是 uint256 類型,定義執行交易所需的簽名數量;nonce 是 uint256 類型,用於防止重放攻擊的遞增計數器;modules 是 mapping(address => bool) 類型,追蹤已啟用的模組。

以下是 Safe 合約的核心介面定義:

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

/**
 * @title ISafe
 * @notice Safe 錢包核心介面定義
 */
interface ISafe {
    /// @notice 獲取錢包的所有者列表
    function getOwners() external view returns (address[] memory);
    
    /// @notice 獲取當前閾值
    function getThreshold() external view returns (uint256);
    
    /// @notice 獲取當前 nonce
    function nonce() external view returns (uint256);
    
    /// @notice 檢查地址是否為所有者
    function isOwner(address owner) external view returns (bool);
    
    /// @notice 執行交易
    /// @param to 目標地址
    /// @param value 轉帳金額(wei)
    /// @param data 調用數據
    /// @param operation 操作類型(0=call, 1=delegatecall)
    /// @param safeTxGas 交易需要的 Gas
    /// @param baseGas 基礎 Gas 費用
    /// @param gasPrice  Gas 價格
    /// @param gasToken 支付 Gas 的代幣地址
    /// @param refundReceiver 退款接收地址
    /// @param signatures 簽名數據
    function execTransaction(
        address to,
        uint256 value,
        bytes calldata data,
        Enum.Operation operation,
        uint256 safeTxGas,
        uint256 baseGas,
        uint256 gasPrice,
        address gasToken,
        address payable refundReceiver,
        bytes memory signatures
    ) external payable returns (bool success);
    
    /// @notice 檢查交易是否已確認
    /// @param hash 交易 hash
    /// @param mode 確認模式
    /// @param 簽名列表
    function checkSignatures(
        bytes32 hash,
        bytes memory signatures,
        bytes memory /* extraData */
    ) external view;
}

1.2 代理模式與升級機制

Safe 使用 UUPS(Universal Upgradeable Proxy Standard,EIP-1822)代理模式來實現可升級性。這種設計選擇有幾個重要優勢:首先,代理合約本身保持不變,用戶的錢包地址在升級後保持一致;其次,升級權限可以完全由治理合約控制,實現去中心化的升級決策;第三,與傳統的可升級代理相比,UUPS 的 gas 效率更高。

SafeProxy 是代理合約,它將所有調用委託給實作合約,同時支持 UUPS 風格的升級:

// 簡化的 SafeProxy 實現
contract SafeProxy {
    bytes32 private constant IMPLEMENTATION_SLOT = 
        bytes32(uint256(keccak256("eip1967.proxy.implementation")) - 1);
    
    constructor(address _implementation, bytes memory _data) {
        _setImplementation(_implementation);
        if (_data.length > 0) {
            _delegate(_implementation);
        }
    }
    
    fallback() external payable {
        _delegate(_getImplementation());
    }
    
    receive() external payable {}
}

SafeSingleton 是實作合約,Safe 團隊會定期發布新的實作版本。每個實作版本都有其獨特的合約地址,用戶可以選擇是否升級到新版本。Safe 的治理合約 SafeDAO 控制著實作升級的發布,確保只有經過充分審計的新版本才會被推荐。

1.3 多重簽名機制

Safe 的多重簽名機制是其安全性的核心。當執行交易時,Safe 合約會驗證足夠數量的所有者已經簽署了交易。這種設計提供了多重安全保障:即使攻擊者盜取了部分私鑰,也無法竊取資金;錢包配置可以通過多簽更改,增強了治理安全性。

交易哈希計算是多重簽名的第一步。Safe 使用以下方式計算交易哈希:

function encodeTransactionData(
    address to,
    uint256 value,
    bytes memory data,
    Enum.Operation operation,
    uint256 safeTxGas,
    uint256 baseGas,
    uint256 gasPrice,
    address gasToken,
    address refundReceiver,
    uint256 _nonce
) public view returns (bytes memory) {
    bytes32 safeTxHash = keccak256(
        abi.encode(
            keccak256(
                abi.encode(
                    to,
                    value,
                    keccak256(data),
                    operation,
                    safeTxGas,
                    baseGas,
                    gasPrice,
                    gasToken,
                    refundReceiver,
                    _nonce
                )
            ),
            address(this),
            block.chainid
        )
    );
    return safeTxHash;
}

簽名驗證支援多種簽名格式:ethsign 格式(原生簽名)、EIP-191 格式(personalsign)、EIP-1271 智慧合約簽名、以及多簽格式(多個簽名拼接)。Safe 的 checkSignatures 函數會根據簽名數據的長度和內容自動識別簽名格式。

1.4 Safe Modules 系統

Safe 的 Module(模組)系統允許擴展錢包功能,而不需要修改核心 Safe 合約。這種設計極大地增加了 Safe 的靈活性,使得第三方開發者可以構建各種創新功能。

ModuleManager 是管理模組的核心合約,提供以下功能:enableModule 啟用新的模組、disableModule 停用模組、execTransactionFromModule 允許模組代表 Safe 執行交易。

常見的 Safe 模組類型包括:延遲交易模組(在執行前添加時間鎖)、角色權限模組(定義不同地址的不同權限)、自動化模組(自動執行預定任務)、社會恢復模組(允許通過守護者網路恢復錢包)。

第二章:Safe Core SDK 完整使用指南

2.1 Safe Core SDK 概述

Safe Core SDK 是官方提供的 JavaScript/TypeScript SDK,用於與 Safe 錢包進行交互。SDK 封裝了底層的合約調用,提供了高層次的 API,使得開發者可以更輕鬆地構建 Safe 相關應用。

Safe Core SDK 的主要套件包括:@safe-global/protocol-kit 處理 Safe 錢包的創建和交互、@safe-global/api-kit 處理 Safe Transaction Service API 的調用、@safe-global/auth-kit 處理錢包連接和認證、@safe-global/onramp-kit 處理法幣入金功能。

2.2 專案設置

首先,創建一個新的 Node.js 專案並安裝必要的依賴:

mkdir safe-app-example
cd safe-app-example
npm init -y
npm install @safe-global/protocol-kit @safe-global/api-kit ethers@6

以下是使用 TypeScript 設置專案的基本結構:

// src/config.ts
import { ethers } from 'ethers';

// 連接配置
export const RPC_URL = process.env.RPC_URL || 'https://mainnet.infura.io/v3/YOUR_PROJECT_ID';
export const CHAIN_ID = 1; // 主網

// 創建 provider
export const provider = new ethers.JsonRpcProvider(RPC_URL);

// Safe 服務配置
export const SAFE_TX_SERVICE_URL = 'https://safe-transaction-mainnet.safe.global';

2.3 Safe 錢包創建

使用 Protocol Kit 創建新的 Safe 錢包:

// src/createSafe.ts
import { SafeFactory } from '@safe-global/protocol-kit';
import { ethers } from 'ethers';

// 連接錢包
const wallet = new ethers.Wallet(process.env.PRIVATE_KEY!, provider);

// 初始化 SafeFactory
const safeFactory = await SafeFactory.init({
  provider: RPC_URL,
  signer: wallet.privateKey
});

// 定義所有者
const owners = [
  '0x...', // 所有者 1 地址
  '0x...', // 所有者 2 地址
  '0x...'  // 所有者 3 地址
];

// 定義閾值(2-of-3 多簽)
const threshold = 2;

// 部署新的 Safe
const safeAccountConfig = {
  owners,
  threshold
};

const safeSdk = await safeFactory.deploySafe({ safeAccountConfig });
const safeAddress = await safeSdk.getAddress();

console.log(`Safe 錢包已部署,地址: ${safeAddress}`);

2.4 交易提案與執行

Safe 的交易流程通常涉及多個步驟:創建交易提案、收集簽名、執行交易。以下是完整的交易流程實現:

// src/transaction.ts
import { Safe } from '@safe-global/protocol-kit';
import { SafeTransactionDataPartial } from '@safe-global/types';

// 連接到現有的 Safe
const safeAddress = '0x...'; // Safe 錢包地址
const safeSdk = await Safe.init({
  provider: RPC_URL,
  signer: wallet.privateKey,
  safeAddress
});

// 定義交易
const transactions: SafeTransactionDataPartial[] = [
  {
    to: '0x...',           // 目標地址
    value: ethers.parseEther('0.1').toString(),  // 轉帳金額
    data: '0x',            // 調用數據
    operation: 0           // 0=call, 1=delegatecall
  }
];

// 創建交易提案
const safeTransaction = await safeSdk.createTransaction({ transactions });

// 添加交易描述
safeTransaction.data = {
  ...safeTransaction.data,
  to: transactions[0].to,
  value: transactions[0].value,
  data: transactions[0].data,
  operation: transactions[0].operation,
  safeTxGas: '0',
  baseGas: '0',
  gasPrice: '0',
  gasToken: '0x0000000000000000000000000000000000000000',
  refundReceiver: wallet.address,
  nonce: await safeSdk.getNonce()
};

// 簽署交易
const signedSafeTx = await safeSdk.signTransaction(safeTransaction);

// 執行交易(需要足夠的簽名)
const executeTxResponse = await safeSdk.executeTransaction(signedSafeTx);
const receipt = await executeTxResponse.transactionResponse?.wait();

console.log(`交易已執行,區塊Hash: ${receipt?.blockHash}`);

2.5 批量交易處理

Safe 支援批量交易,即單個交易包含多個子調用。這對於需要在同一個交易中執行多個操作的場景非常有用,例如:approve + swap 的組合、去中心化交易所的多步操作。

// src/batchTransaction.ts

// 定義批量交易
const batchTransactions: SafeTransactionDataPartial[] = [
  {
    to: '0xTokenContract',     // ERC-20 代幣合約
    value: '0',
    data: encodeApprove(spenderAddress, amount),  // 批准
    operation: 0
  },
  {
    to: '0xDEXContract',        // DEX 合約
    value: '0',
    data: encodeSwap(tokenIn, tokenOut, amount),  // 兌換
    operation: 0
  },
  {
    to: '0xNFTContract',        // NFT 合約
    value: ethers.parseEther('0.05').toString(),
    data: encodeMint(tokenId),  // 鑄造 NFT
    operation: 0
  }
];

// 創建批量交易
const batchTransaction = await safeSdk.createTransaction({
  transactions: batchTransactions,
  options: {
    safeTxGas: '100000'  // 估算的 Gas
  }
});

// 簽署並執行
const signedTx = await safeSdk.signTransaction(batchTransaction);
const result = await safeSdk.executeTransaction(signedTx);

2.6 提議者與確認查詢

Safe 的治理機制通常涉及「提議者」(Proposer)和「確認者」(Approver)的角色。使用 API Kit 可以查詢錢包的待處理交易和確認狀態:

// src/queries.ts
import { SafeApiKit } from '@safe-global/api-kit';

// 初始化 API Kit
const safeApiKit = new SafeApiKit({
  chainId: BigInt(CHAIN_ID)
});

// 獲取 Safe 的待處理交易
const pendingTransactions = await safeApiKit.getPendingTransactions(safeAddress);
console.log('待處理交易:', pendingTransactions.results);

// 獲取交易歷史
const txHistory = await safeApiKit.getTransactionHistory(safeAddress);
console.log('交易歷史:', txHistory.results);

// 獲取所有權者和閾值
const safeInfo = await safeApiKit.getSafeInfo(safeAddress);
console.log('錢包配置:', {
  owners: safeInfo.owners,
  threshold: safeInfo.threshold,
  nonce: safeInfo.nonce
});

// 查詢特定交易的確認狀態
const confirmations = await safeApiKit.getTransactionConfirmations(txHash);
console.log('確認狀態:', confirmations.confirmations);

第三章:Safe 模組開發完整指南

3.1 Safe 模組設計原則

Safe 模組是擴展 Safe 錢包功能的合約,可以代表 Safe 錢包執行交易。模組設計需要遵循幾個重要原則:最小權限原則(模組只應獲得完成其功能所需的最小權限)、失敗安全設計(模組應設計失敗安全,避免因單點故障導致資金損失)、無阻塞設計(模組不應阻止 Safe 錢包的正常運作)。

模組與 Guard 的區別:模組可以主動發起交易,類似於「功能擴展」;而 Guard 是交易鉤子,在交易執行前進行檢查,類似於「安全過濾器」。選擇使用模組還是 Guard 取決於具體的使用場景。

3.2 基礎模組合約框架

以下是創建 Safe 模組的基本框架:

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

import "@openzeppelin/contracts/access/Ownable.sol";
import "@openzeppelin/contracts/security/ReentrancyGuard.sol";

/**
 * @title BaseSafeModule
 * @notice Safe 模組基礎合約
 */
abstract contract BaseSafeModule is Ownable, ReentrancyGuard {
    
    // Safe 模組介面
    ISafe public safe;
    
    // 模組配置
    bool public isInitialized;
    
    // 事件
    event ModuleInitialized(address indexed safe, address indexed owner);
    event ModuleExecution(address indexed to, uint256 value, bytes data);
    
    /**
     * @notice 初始化模組
     * @param _safe Safe 錢包地址
     * @param _owner 模組管理員地址
     */
    function initialize(address _safe, address _owner) external virtual {
        require(!isInitialized, "Already initialized");
        require(_safe != address(0), "Invalid Safe address");
        
        safe = ISafe(_safe);
        isInitialized = true;
        
        // 設置 Ownable 的所有者
        if (_owner != address(0)) {
            _transferOwnership(_owner);
        }
        
        emit ModuleInitialized(_safe, _owner);
    }
    
    /**
     * @notice 執行交易(代表 Safe)
     * @param to 目標地址
     * @param value 轉帳金額
     * @param data 調用數據
     */
    function execTransaction(
        address to,
        uint256 value,
        bytes memory data
    ) internal returns (bool success) {
        // 使用 Safe 的 execTransactionFromModule
        success = safe.execTransactionFromModule(to, value, data, 0);
        require(success, "Module transaction failed");
        emit ModuleExecution(to, value, data);
    }
    
    /**
     * @notice 執行批量交易
     */
    function execBatchTransactions(
        address[] memory tos,
        uint256[] memory values,
        bytes[] memory datas
    ) internal nonReentrant {
        require(tos.length == values.length, "Length mismatch");
        require(tos.length == datas.length, "Length mismatch");
        
        for (uint256 i = 0; i < tos.length; i++) {
            execTransaction(tos[i], values[i], datas[i]);
        }
    }
}

3.3 延遲交易模組開發

延遲交易模組是 Safe 生態系統中常見的模組類型,它在交易執行前添加時間鎖,提高安全性。以下是完整的實現:

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

import "./BaseSafeModule.sol";

/**
 * @title TimeLockModule
 * @notice 延遲交易模組
 * @dev 所有交易在執行前都需要經過配置的延遲期
 */
contract TimeLockModule is BaseSafeModule {
    
    // 交易請求結構
    struct TransactionRequest {
        address to;
        uint256 value;
        bytes data;
        uint256 scheduleTime;
        uint256 executionTime;
        address proposer;
        bool executed;
        bytes32 txHash;
    }
    
    // 配置參數
    uint256 public minDelay;      // 最小延遲(秒)
    uint256 public maxDelay;      // 最大延遲(秒)
    uint256 public defaultDelay;  // 預設延遲
    
    // 交易映射
    mapping(bytes32 => TransactionRequest) public transactionRequests;
    bytes32[] public transactionQueue;
    
    // 事件
    event TransactionQueued(
        bytes32 indexed txHash,
        address indexed proposer,
        uint256 executionTime
    );
    event TransactionExecuted(bytes32 indexed txHash);
    event TransactionCancelled(bytes32 indexed txHash);
    
    /**
     * @notice 初始化模組
     */
    function initialize(
        address _safe,
        address _owner,
        uint256 _defaultDelay,
        uint256 _minDelay,
        uint256 _maxDelay
    ) external override {
        super.initialize(_safe, _owner);
        
        require(_minDelay <= _maxDelay, "Invalid delay range");
        require(_defaultDelay >= _minDelay && _defaultDelay <= _maxDelay, "Invalid default delay");
        
        defaultDelay = _defaultDelay;
        minDelay = _minDelay;
        maxDelay = _maxDelay;
    }
    
    /**
     * @notice 提議新交易
     * @param to 目標地址
     * @param value 轉帳金額
     * @param data 調用數據
     * @param delay 自定義延遲(0 表示使用預設值)
     */
    function proposeTransaction(
        address to,
        uint256 value,
        bytes calldata data,
        uint256 delay
    ) external onlyOwner returns (bytes32 txHash) {
        // 計算延遲
        uint256 actualDelay = delay == 0 ? defaultDelay : delay;
        require(actualDelay >= minDelay && actualDelay <= maxDelay, "Delay out of range");
        
        // 生成交易哈希
        txHash = keccak256(abi.encode(
            to, value, data, block.timestamp, msg.sender
        ));
        
        // 創建交易請求
        transactionRequests[txHash] = TransactionRequest({
            to: to,
            value: value,
            data: data,
            scheduleTime: block.timestamp,
            executionTime: block.timestamp + actualDelay,
            proposer: msg.sender,
            executed: false,
            txHash: txHash
        });
        
        transactionQueue.push(txHash);
        
        emit TransactionQueued(txHash, msg.sender, block.timestamp + actualDelay);
    }
    
    /**
     * @notice 執行已通過延遲期的交易
     */
    function executeTransaction(bytes32 txHash) external nonReentrant {
        TransactionRequest storage request = transactionRequests[txHash];
        
        require(request.executionTime > 0, "Transaction not found");
        require(!request.executed, "Already executed");
        require(block.timestamp >= request.executionTime, "Not yet executable");
        
        request.executed = true;
        
        // 執行交易
        execTransaction(request.to, request.value, request.data);
        
        emit TransactionExecuted(txHash);
    }
    
    /**
     * @notice 取消交易
     */
    function cancelTransaction(bytes32 txHash) external onlyOwner {
        TransactionRequest storage request = transactionRequests[txHash];
        
        require(request.executionTime > 0, "Transaction not found");
        require(!request.executed, "Already executed");
        
        delete transactionRequests[txHash];
        
        emit TransactionCancelled(txHash);
    }
    
    /**
     * @notice 查詢待執行的交易
     */
    function getExecutableTransactions() external view returns (bytes32[] memory) {
        uint256 count = 0;
        
        // 計算可執行交易數量
        for (uint256 i = 0; i < transactionQueue.length; i++) {
            TransactionRequest storage request = transactionRequests[transactionQueue[i]];
            if (!request.executed && block.timestamp >= request.executionTime) {
                count++;
            }
        }
        
        // 構建結果數組
        bytes32[] memory result = new bytes32[](count);
        uint256 index = 0;
        
        for (uint256 i = 0; i < transactionQueue.length; i++) {
            TransactionRequest storage request = transactionRequests[transactionQueue[i]];
            if (!request.executed && block.timestamp >= request.executionTime) {
                result[index++] = transactionQueue[i];
            }
        }
        
        return result;
    }
}

3.4 角色權限模組開發

角色權限模組允許定義不同的角色,每個角色有不同的權限限制。這對於組織使用 Safe 錢包非常有用,可以實現精細的權限控制:

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

import "./BaseSafeModule.sol";
import "@openzeppelin/contracts/access/AccessControl.sol";

/**
 * @title RoleBasedModule
 * @notice 基於角色的權限模組
 * @dev 定義不同角色的轉帳限額和目標限制
 */
contract RoleBasedModule is BaseSafeModule, AccessControl {
    
    // 角色定義
    bytes32 public constant ADMIN_ROLE = keccak256("ADMIN_ROLE");
    bytes32 public constant SPENDER_ROLE = keccak256("SPENDER_ROLE");
    bytes32 public constant VIEWER_ROLE = keccak256("VIEWER_ROLE");
    
    // 角色配置
    struct RoleConfig {
        uint256 dailyLimit;      // 每日限額
        uint256 txLimit;         // 單筆限額
        bool whitelistEnabled;   // 是否啟用白名單
        mapping(address => bool) allowedTargets;
    }
    
    // 角色映射
    mapping(bytes32 => RoleConfig) public roleConfigs;
    
    // 交易記錄
    mapping(bytes32 => mapping(address => uint256)) public dailySpent;  // role -> token -> amount
    mapping(bytes32 => mapping(address => uint256)) public lastResetTime;
    
    // 事件
    event RoleConfigured(bytes32 indexed role, uint256 dailyLimit, uint256 txLimit);
    event TargetWhitelisted(bytes32 indexed role, address indexed target, bool allowed);
    event RoleSpend(address indexed role, address indexed to, uint256 amount);
    
    /**
     * @notice 初始化模組
     */
    function initialize(
        address _safe,
        address _admin
    ) external override {
        super.initialize(_safe, address(0));
        
        // 設置默認角色
        _grantRole(ADMIN_ROLE, _admin);
        _grantRole(ADMIN_ROLE, _safe);
        
        setRoleConfig(SPENDER_ROLE, 10 ether, 1 ether, true);
        setRoleConfig(VIEWER_ROLE, 0, 0, false);
        
        isInitialized = true;
    }
    
    /**
     * @notice 配置角色
     */
    function setRoleConfig(
        bytes32 role,
        uint256 dailyLimit,
        uint256 txLimit,
        bool whitelistEnabled
    ) public onlyRole(ADMIN_ROLE) {
        RoleConfig storage config = roleConfigs[role];
        config.dailyLimit = dailyLimit;
        config.txLimit = txLimit;
        config.whitelistEnabled = whitelistEnabled;
        
        emit RoleConfigured(role, dailyLimit, txLimit);
    }
    
    /**
     * @notice 更新白名單
     */
    function setTargetWhitelist(
        bytes32 role,
        address[] calldata targets,
        bool[] calldata allowed
    ) external onlyRole(ADMIN_ROLE) {
        require(targets.length == allowed.length, "Length mismatch");
        
        for (uint256 i = 0; i < targets.length; i++) {
            roleConfigs[role].allowedTargets[targets[i]] = allowed[i];
            emit TargetWhitelisted(role, targets[i], allowed[i]);
        }
    }
    
    /**
     * @notice 代表 Safe 執行交易(帶權限檢查)
     */
    function executeWithRole(
        bytes32 role,
        address to,
        uint256 value,
        bytes calldata data
    ) external nonReentrant onlyRole(role) returns (bool) {
        RoleConfig storage config = roleConfigs[role];
        
        // 檢查單筆限額
        if (value > 0) {
            require(value <= config.txLimit || config.txLimit == 0, "Exceeds tx limit");
        }
        
        // 檢查白名單
        if (config.whitelistEnabled && config.allowedTargets[to]) {
            require(config.allowedTargets[to], "Target not whitelisted");
        }
        
        // 檢查每日限額
        if (config.dailyLimit > 0 && value > 0) {
            _checkDailyLimit(role, to, value);
        }
        
        // 更新花費記錄
        if (config.dailyLimit > 0 && value > 0) {
            dailySpent[role][to] += value;
        }
        
        // 執行交易
        bool success = safe.execTransactionFromModule(to, value, data, 0);
        require(success, "Execution failed");
        
        emit RoleSpend(role, to, value);
        
        return true;
    }
    
    /**
     * @notice 檢查每日限額
     */
    function _checkDailyLimit(
        bytes32 role,
        address token,
        uint256 amount
    ) internal {
        RoleConfig storage config = roleConfigs[role];
        
        // 重置計時器(如需要)
        if (block.timestamp - lastResetTime[role][token] >= 24 hours) {
            dailySpent[role][token] = 0;
            lastResetTime[role][token] = block.timestamp;
        }
        
        require(
            dailySpent[role][token] + amount <= config.dailyLimit,
            "Exceeds daily limit"
        );
    }
}

3.5 自動化 keeper 任務

Safe 的 Keepers 功能允許模組自動執行預定任務,如定期質押收益領取、自動化 DeFi 操作等。以下是 keeper 任務模組的實現:

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

import "./BaseSafeModule.sol";

/**
 * @title KeeperModule
 * @notice 自動化 keeper 任務模組
 */
contract KeeperModule is BaseSafeModule {
    
    // 任務配置
    struct Task {
        address target;           // 目標合約
        bytes data;              // 調用數據
        uint256 interval;        // 執行間隔(秒)
        uint256 lastExecution;    // 上次執行時間
        bool enabled;            // 是否啟用
        address keeper;          // 指定的 keeper 地址
    }
    
    // 任務映射
    mapping(bytes32 => Task) public tasks;
    bytes32[] public taskIds;
    
    // Keepers 列表
    mapping(address => bool) public keepers;
    
    // 獎勵配置
    uint256 public keeperReward;  // 每次執行的獎勵
    
    // 事件
    event TaskCreated(bytes32 indexed taskId, address indexed target);
    event TaskExecuted(bytes32 indexed taskId, uint256 timestamp);
    event TaskCancelled(bytes32 indexed taskId);
    event KeeperRegistered(address indexed keeper);
    
    /**
     * @notice 初始化模組
     */
    function initialize(
        address _safe,
        address _owner,
        uint256 _keeperReward
    ) external override {
        super.initialize(_safe, _owner);
        keeperReward = _keeperReward;
        isInitialized = true;
    }
    
    /**
     * @notice 註冊 keeper
     */
    function registerKeeper(address _keeper) external onlyOwner {
        keepers[_keeper] = true;
        emit KeeperRegistered(_keeper);
    }
    
    /**
     * @notice 創建自動化任務
     */
    function createTask(
        bytes32 taskId,
        address target,
        bytes calldata data,
        uint256 interval,
        address _keeper
    ) external onlyOwner returns (bytes32) {
        require(tasks[taskId].lastExecution == 0, "Task already exists");
        require(interval > 0, "Invalid interval");
        
        tasks[taskId] = Task({
            target: target,
            data: data,
            interval: interval,
            lastExecution: block.timestamp,
            enabled: true,
            keeper: _keeper
        });
        
        taskIds.push(taskId);
        
        emit TaskCreated(taskId, target);
        
        return taskId;
    }
    
    /**
     * @notice 執行到期任務
     */
    function executeTask(bytes32 taskId) external nonReentrant {
        Task storage task = tasks[taskId];
        
        require(task.lastExecution > 0, "Task not found");
        require(task.enabled, "Task disabled");
        require(
            keepers[msg.sender] || msg.sender == task.keeper,
            "Not authorized keeper"
        );
        require(
            block.timestamp >= task.lastExecution + task.interval,
            "Not yet due"
        );
        
        // 更新執行時間
        task.lastExecution = block.timestamp;
        
        // 執行任務
        safe.execTransactionFromModule(task.target, 0, task.data, 0);
        
        emit TaskExecuted(taskId, block.timestamp);
        
        // 支付 keeper 獎勵
        if (keeperReward > 0) {
            safe.execTransactionFromModule(
                msg.sender,
                keeperReward,
                "",
                0
            );
        }
    }
    
    /**
     * @notice 批量執行到期任務
     */
    function executeTasks(bytes32[] calldata taskIds) external nonReentrant {
        uint256 count = 0;
        
        for (uint256 i = 0; i < taskIds.length; i++) {
            bytes32 taskId = taskIds[i];
            Task storage task = tasks[taskId];
            
            if (
                task.lastExecution > 0 &&
                task.enabled &&
                block.timestamp >= task.lastExecution + task.interval &&
                (keepers[msg.sender] || msg.sender == task.keeper)
            ) {
                task.lastExecution = block.timestamp;
                safe.execTransactionFromModule(task.target, 0, task.data, 0);
                count++;
                
                emit TaskExecuted(taskId, block.timestamp);
            }
        }
        
        // 統一支付獎勵
        if (keeperReward > 0 && count > 0) {
            safe.execTransactionFromModule(
                msg.sender,
                keeperReward * count,
                "",
                0
            );
        }
    }
    
    /**
     * @notice 取消任務
     */
    function cancelTask(bytes32 taskId) external onlyOwner {
        require(tasks[taskId].lastExecution > 0, "Task not found");
        
        delete tasks[taskId];
        
        emit TaskCancelled(taskId);
    }
    
    /**
     * @notice 獲取可執行的任務
     */
    function getExecutableTasks() external view returns (bytes32[] memory) {
        uint256 count = 0;
        
        for (uint256 i = 0; i < taskIds.length; i++) {
            Task storage task = tasks[taskIds[i]];
            if (
                task.enabled &&
                block.timestamp >= task.lastExecution + task.interval
            ) {
                count++;
            }
        }
        
        bytes32[] memory result = new bytes32[](count);
        uint256 index = 0;
        
        for (uint256 i = 0; i < taskIds.length; i++) {
            Task storage task = tasks[taskIds[i]];
            if (
                task.enabled &&
                block.timestamp >= task.lastExecution + task.interval
            ) {
                result[index++] = taskIds[i];
            }
        }
        
        return result;
    }
}

第四章:Safe 開發環境配置

4.1 Foundry 開發環境設置

Foundry 是目前以太坊開發者最喜愛的開發框架之一,以下是使用 Foundry 開發 Safe 應用的配置:

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

# 創建專案
forge init safe-module-example
cd safe-module-example

# 添加依賴
forge install safe-global/safe-smart-account --no-commit
forge install OpenZeppelin/openzeppelin-contracts --no-commit

4.2 Hardhat 配置

對於習慣 Hardhat 的開發者,以下是配置示例:

// hardhat.config.js
require("@nomicfoundation/hardhat-toolbox");
require("@safe-global/safe-ethers-libs");

/** @type import('hardhat/config').HardhatUserConfig */
module.exports = {
  solidity: {
    version: "0.8.19",
    settings: {
      optimizer: {
        enabled: true,
        runs: 200
      }
    }
  },
  networks: {
    mainnet: {
      url: process.env.MAINNET_RPC_URL,
      accounts: [process.env.PRIVATE_KEY]
    },
    sepolia: {
      url: process.env.SEPOLIA_RPC_URL,
      accounts: [process.env.PRIVATE_KEY]
    }
  },
  safe: {
    safeAddress: "0x41675C0733a06D8284A1d60dB1d2F30d8F8F9aF5" // Safe Singleton
  }
};

4.3 測試網路部署腳本

以下是使用 Foundry 部署 Safe 模組的完整腳本:

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

import "forge-std/Script.sol";
import "forge-std/console.sol";
import "../../contracts/modules/TimeLockModule.sol";
import "../../contracts/modules/RoleBasedModule.sol";

/**
 * @title SafeModuleDeployment
 * @notice Safe 模組部署腳本
 */
contract SafeModuleDeployment is Script {
    
    // 部署參數
    struct DeploymentConfig {
        address safeAddress;      // Safe 錢包地址
        address owner;            // 模組管理員
        uint256 defaultDelay;     // 預設延遲(秒)
        uint256 minDelay;         // 最小延遲
        uint256 maxDelay;         // 最大延遲
    }
    
    function run(DeploymentConfig memory config) external {
        console.log("部署 Safe 模組...");
        console.log("Safe 地址:", config.safeAddress);
        
        vm.startBroadcast();
        
        // 部署時間鎖模組
        TimeLockModule timeLockModule = new TimeLockModule();
        timeLockModule.initialize(
            config.safeAddress,
            config.owner,
            config.defaultDelay,
            config.minDelay,
            config.maxDelay
        );
        console.log("TimeLockModule 地址:", address(timeLockModule));
        
        // 部署角色權限模組
        RoleBasedModule roleModule = new RoleBasedModule();
        roleModule.initialize(config.safeAddress, config.owner);
        console.log("RoleBasedModule 地址:", address(roleModule));
        
        vm.stopBroadcast();
        
        console.log("部署完成!");
        console.log("");
        console.log("後續步驟:");
        console.log("1. 在 Safe 錢包中啟用模組:");
        console.log("   - TimeLockModule:", address(timeLockModule));
        console.log("   - RoleBasedModule:", address(roleModule));
        console.log("2. 通過 Safe 錢包調用 enableModule");
    }
}

執行部署腳本:

# 部署到 Sepolia 測試網路
forge script script/DeploySafeModule.s.sol: SafeModuleDeployment \
  --rpc-url $SEPOLIA_RPC_URL \
  --private-key $PRIVATE_KEY \
  --broadcast \
  -vvv

第五章:Safe 開發常見問題與解決方案

5.1 模組啟用失敗

問題:執行 enableModule 交易時失敗。

可能原因:Safe 合約不允許將自身設為模組;缺少足夠的簽名;交易 Gas 不足。

解決方案

// 檢查 Safe 錢包配置
const safeInfo = await safeApiKit.getSafeInfo(safeAddress);

// 檢查當前簽名數量
console.log(`當前閾值: ${safeInfo.threshold}`);
console.log(`所有者數量: ${safeInfo.owners.length}`);

// 確保收集足夠的簽名
const safeSdk = await Safe.init({
  provider: RPC_URL,
  signer: signer,
  safeAddress
});

// 創建啟用模組的交易
const enableModuleTx = await safeSdk.createEnableModuleTx(moduleAddress);

// 簽署並執行
const signedTx = await safeSdk.signTransaction(enableModuleTx);
await safeSdk.executeTransaction(signedTx);

5.2 交易執行順序問題

問題:批量交易中的某些子交易失敗導致整個交易回滾。

解決方案:使用 Safe 的 isModuleEnabled 檢查和 try-catch 包裝:

// 在模組中安全地執行批量交易
function execBatchTransactionsSafe(
    address[] memory tos,
    uint256[] memory values,
    bytes[] memory datas
) internal returns (uint256 successCount, uint256 failCount) {
    for (uint256 i = 0; i < tos.length; i++) {
        try safe.execTransactionFromModule(tos[i], values[i], datas[i], 0) {
            successCount++;
        } catch {
            failCount++;
            // 記錄失敗但繼續執行
        }
    }
    
    require(successCount > 0, "All transactions failed");
}

5.3 Gas 估算問題

問題:複雜交易的 Gas 估算不準確。

解決方案

// 使用 Safe 的 safeTxGas 估算
const safeTransaction = await safeSdk.createTransaction({
  transactions: [tx]
});

// 獲取估算的 Gas
const gas = await provider.estimateGas({
  to: safeAddress,
  from: safeAddress,
  data: safeSdk.encodeSafeTransaction(safeTransaction)
});

// 添加安全邊界
const safeTxGas = Math.ceil(Number(gas) * 1.2);

結論

Safe{Wallet} 提供了以太坊生態系統中最成熟和安全的智慧合約錢包解決方案。其模組化架構使得開發者可以構建各種類型的擴展功能,從簡單的延遲交易到複雜的自動化任務系統。

本文涵蓋了 Safe 開發的核心主題:從錢包架構理解、SDK 使用、模組開發到部署流程。讀者可以基於這些知識構建自己的 Safe 應用和擴展功能。

開發 Safe 應用時,請始終牢記安全優先的原則:充分測試所有功能、使用最小權限設計、定期審計模組代碼。Safe 團隊提供了詳盡的開發文檔和安全指南,建議開發者在投入生產環境前仔細閱讀。

參考資源


本文最後更新時間:2026年3月

延伸閱讀與來源

這篇文章對您有幫助嗎?

評論

發表評論

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

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