多签钱包(Multi-Signature Wallet)是一种需要多个私钥持有者共同批准才能执行交易的智能合约。它广泛用于提高资金安全性,避免单点故障(如单个私钥泄露)。在 Solidity 中实现多签钱包,核心思路是:预设一组所有者(owners)和一个阈值(threshold),只有当足够多的所有者确认一笔交易后,该交易才能被执行。
1. 基本原理
- 所有者(Owners): 有权批准交易的地址列表,通常不可更改(或通过多签本身修改)。
- 阈值(Threshold): 执行交易所需的最少确认数,通常≤ 所有者数量。
- 交易(Transaction): 包括i目标地址、发送的以太币数量、调用数据(data)以及一个唯一标识(如交易ID或nonce)。
- 确认(Confirmations): 所有者调用合约方法对某笔交易表示同意。当确认数达到阈值时,任何人均可触发执行。
2. 核心功能与实现要点
2.1 数据结构
transactions数组存储所有待处理或已执行的交易(包含交易目标地址、发送金额、交易数据和执行状态)。isConfirmed记录每个地址对每笔交易的确认状态(记录每个地址对每笔交易的确认状态)。numConfirmations可动态计数,也可通过遍历计算(但遍历消耗gas)。
solidity
address[] public owners;
mapping(address => bool) public isOwner;
uint public threshold;
struct Transaction {
address to;
uint value;
bytes data;
bool executed;
uint numConfirmations;
}
mapping(uint => mapping(address => bool)) public isConfirmed;
Transaction[] public transactions;
2.2 提交交易
- 通常只允许所有者提交交易,但也可开放给任何人(需防止垃圾交易)。
solidity
function submitTransaction(address _to, uint _value, bytes memory _data) public onlyOwner returns (uint txIndex) {
txIndex = transactions.length;
transactions.push(Transaction({
to: _to,
value: _value,
data: _data,
executed: false,
numConfirmations: 0
}));
emit SubmitTransaction(msg.sender, txIndex, _to, _value, _data);
}
2.3 确认交易
solidity
function confirmTransaction(uint _txIndex) public onlyOwner {
require(_txIndex < transactions.length, "tx does not exist");
Transaction storage txn = transactions[_txIndex];
require(!txn.executed, "tx already executed");
require(!isConfirmed[_txIndex][msg.sender], "tx already confirmed");
isConfirmed[_txIndex][msg.sender] = true;
txn.numConfirmations += 1;
emit ConfirmTransaction(msg.sender, _txIndex);
}
2.4 执行交易
- 调用
call可同时发送 ETH 并调用目标合约函数。 - 注意重入攻击:在状态更改后再进行外部调用,并使用
executed标志防止重复执行。
solidity
function executeTransaction(uint _txIndex) public {
require(_txIndex < transactions.length, "tx does not exist");
Transaction storage txn = transactions[_txIndex];
require(!txn.executed, "tx already executed");
require(txn.numConfirmations >= threshold, "not enough confirmations");
txn.executed = true;
(bool success, ) = txn.to.call{value: txn.value}(txn.data);
require(success, "tx failed");
emit ExecuteTransaction(msg.sender, _txIndex);
}
2.5 撤销确认(可选)
solidity
function revokeConfirmation(uint _txIndex) public onlyOwner {
require(_txIndex < transactions.length, "tx does not exist");
Transaction storage txn = transactions[_txIndex];
require(!txn.executed, "tx already executed");
require(isConfirmed[_txIndex][msg.sender], "tx not confirmed");
isConfirmed[_txIndex][msg.sender] = false;
txn.numConfirmations -= 1;
emit RevokeConfirmation(msg.sender, _txIndex);
}
2.6 修改所有者与阈值(高级)
可以通过多签本身来实现,即提交一笔调用 addOwner、removeOwner 或 changeThreshold 的交易,由现有所有者批准后执行。
3. 安全注意事项
- 重入攻击 :执行交易时使用"检查-生效-交互"模式,先标记
executed = true再调用外部地址。 - Gas 限制:交易数组可能很大,避免遍历所有确认;可使用计数器优化。
- 所有权管理:初始所有者需在构造函数中设定,后续修改需谨慎。
- 拒绝服务:恶意所有者可无限提交垃圾交易,但可通过限制提交权限或收取费用缓解。
- 使用事件:所有关键操作(提交、确认、执行、撤销)都应记录事件,便于链下监听。
4. 简化示例代码
以下是一个极简多签钱包的核心合约(省略了事件、修饰器等):
solidity
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract MultiSigWallet {
address[] public owners;
mapping(address => bool) public isOwner;
uint public threshold;
struct Transaction {
address to;
uint value;
bytes data;
bool executed;
uint confirmCount;
}
Transaction[] public transactions;
mapping(uint => mapping(address => bool)) public confirmed;
modifier onlyOwner() {
require(isOwner[msg.sender], "not owner");
_;
}
constructor(address[] memory _owners, uint _threshold) {
require(_owners.length > 0, "owners required");
require(_threshold > 0 && _threshold <= _owners.length, "invalid threshold");
for (uint i = 0; i < _owners.length; i++) {
address owner = _owners[i];
require(owner != address(0) && !isOwner[owner], "invalid owner");
isOwner[owner] = true;
owners.push(owner);
}
threshold = _threshold;
}
function submitTransaction(address _to, uint _value, bytes memory _data) public onlyOwner returns (uint txIndex) {
txIndex = transactions.length;
transactions.push(Transaction({
to: _to,
value: _value,
data: _data,
executed: false,
confirmCount: 0
}));
}
function confirmTransaction(uint _txIndex) public onlyOwner {
require(_txIndex < transactions.length, "invalid tx");
Transaction storage txn = transactions[_txIndex];
require(!txn.executed, "executed");
require(!confirmed[_txIndex][msg.sender], "already confirmed");
confirmed[_txIndex][msg.sender] = true;
txn.confirmCount += 1;
}
function executeTransaction(uint _txIndex) public {
require(_txIndex < transactions.length, "invalid tx");
Transaction storage txn = transactions[_txIndex];
require(!txn.executed, "executed");
require(txn.confirmCount >= threshold, "not enough confirmations");
txn.executed = true;
(bool success, ) = txn.to.call{value: txn.value}(txn.data);
require(success, "tx failed");
}
// 接收 ETH 的函数
receive() external payable {}
}
5. 现有成熟方案
- Gnosis Safe:目前最流行的多签钱包,支持模块化、多种链、合约交互,并有完善的UI。其合约经过严格审计,推荐直接使用。
- 其他实现 :如 OpenZeppelin 也提供
MultisigWallet合约示例,但已较少维护。
6. 总结
实现多签钱包的核心是管理交易生命周期:提交 → 多方确认 → 达到阈值后执行。关键点在于确保只有所有者可操作,防止重入,并合理设计数据结构以节省 Gas。对于生产环境,建议直接采用经过实战检验的方案(如 Gnosis Safe),避免自行实现时的安全风险。