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 團隊提供了詳盡的開發文檔和安全指南,建議開發者在投入生產環境前仔細閱讀。
參考資源
- Safe Developer Documentation: https://docs.safe.global
- Safe Core SDK: https://github.com/safe-global/safe-core-sdk
- Safe Smart Account: https://github.com/safe-global/safe-smart-account
- Safe Module Examples: https://github.com/safe-global/safe-modules
本文最後更新時間:2026年3月
相關文章
- 以太坊帳戶抽象實際採用案例與使用者痛點完整解決方案指南 — 帳戶抽象(Account Abstraction)是以太坊使用者體驗革命的核心技術。透過 ERC-4337 標準的推廣,智慧合約錢包正在逐步取代傳統的外部擁有帳戶(EOA),為用戶帶來更安全的資產管理、更便利的恢復機制和更靈活的交易控制。本文深入分析帳戶抽象在 2025-2026 年的實際採用情況,探討當前用戶面臨的核心痛點,並提供從技術架構到產品設計的完整解決方案。
- 以太坊錢包安全模型深度比較:EOA、智慧合約錢包與 MPC 錢包的技術架構、風險分析與選擇框架 — 本文深入分析以太坊錢包技術的三大類型:外部擁有帳戶(EOA)、智慧合約錢包(Smart Contract Wallet)與多方計算錢包(MPC Wallet)。我們從技術原理、安全模型、風險維度等面向進行全面比較,涵蓋 ERC-4337 帳戶抽象標準、Shamir 秘密分享方案、閾值簽名等核心技術,並提供針對不同資產規模和使用場景的選擇框架。截至 2026 年第一季度,以太坊生態系統的錢包技術持續演進,理解這些技術差異對於保護數位資產至關重要。
- 社交恢復錢包技術實作完整指南:智慧合約錢包架構、守護者機制與安全設計深度分析 — 社交恢復錢包解決了傳統加密貨幣錢包的核心痛點:私鑰遺失導致資產永久無法訪問的問題。本文深入分析社交恢復錢包的技術架構,包括智慧合約實現、守護者機制設計、恢復流程、安全考量等各個層面,提供完整的程式碼範例和安全分析。
- 以太坊錢包安全實務進階指南:合約錢包與 EOA 安全差異、跨鏈橋接風險評估 — 本文深入探討以太坊錢包的安全性實務,特別聚焦於合約錢包與外部擁有帳戶(EOA)的安全差異分析,以及跨鏈橋接的風險評估方法。我們將從密碼學基礎出發,詳細比較兩種帳戶類型的安全模型,並提供完整的程式碼範例展示如何實現安全的多重簽名錢包。同時,本文系統性地分析跨鏈橋接面臨的各類風險,提供風險評估框架和最佳實踐建議,幫助讀者建立全面的錢包安全知識體系。
- ERC-4337 Bundler 完整實作指南:從原理到部署 — ERC-4337(帳戶抽象標準)是以太坊帳戶模型的重要革新,其核心創新是將帳戶驗證邏輯從共識層分離到應用層。在這個架構中,Bundler(捆綁器)是關鍵的基礎設施元件,負責收集用戶操作(UserOperation)、將其打包並提交到 EntryPoint 合約執行。本文深入解析 Bundler 的運作原理、核心元件的程式碼實作、以及部署與運維的最佳實踐。
延伸閱讀與來源
- Ethereum.org Developers 官方開發者入口與技術文件
- EIPs 以太坊改進提案完整列表
- Solidity 文檔 智慧合約程式語言官方規格
- EVM 代碼庫 EVM 實作的核心參考
- Alethio EVM 分析 EVM 行為的正規驗證
這篇文章對您有幫助嗎?
請告訴我們如何改進:
評論
發表評論
注意:由於這是靜態網站,您的評論將儲存在本地瀏覽器中,不會公開顯示。
目前尚無評論,成為第一個發表評論的人吧!