今天我们要聊一个在区块链开发中超级重要且实用的主题------多签钱包(Multi-Signature Wallet)。如果你玩过DeFi、DAO或者团队管理的加密资产,肯定听说过多签钱包。它就像一个"多人保险箱",需要多个签名者同意才能动用资金,极大地提高了安全性和去中心化特性。
多签钱包是什么?为什么需要它?
多签钱包(Multi-Signature Wallet)是一种智能合约,要求多方(而不是单一地址)共同签名才能执行关键操作,比如转账、修改配置等。它的核心思想是"分散信任",避免单点故障。想象一下,一个DAO的资金由一个私钥控制,如果这个私钥丢了或被盗,整个项目就GG了!多签钱包通过要求M-of-N签名(比如3-of-5,5个签名者中至少3个同意)来降低风险。
在Solidity中,多签钱包通常用于:
- 团队资金管理:比如DAO的国库需要核心成员共同批准才能支出。
- 安全保障:防止单一管理员失误或恶意操作。
- 去中心化治理:投票决定合约行为,比如升级或参数调整。
- 托管服务:在交易中确保资金安全,只有多方确认后释放。
多签钱包的核心功能包括:
- 提交提案:某个签名者提出一个交易(比如转ETH)。
- 确认提案:其他签名者投票支持。
- 执行交易:达到所需签名数后自动执行。
- 权限管理:添加或移除签名者,调整签名要求。
接下来,我们会实现一个多签钱包合约,逐步分析每个功能。
实现一个基础多签钱包
为了让大家快速上手,我们来写一个多签钱包合约MultiSigWallet
,功能包括:
- 支持多个签名者(owners),需要指定最少签名数(required)。
- 签名者可以提交交易提案(转账ETH)。
- 签名者确认提案,达到所需签名数后执行。
- 查询提案状态和历史。
基础合约结构
先来看合约的框架,包含核心状态变量和初始化逻辑:
solidity
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract MultiSigWallet {
address[] public owners;
uint public required;
mapping(address => bool) public isOwner;
uint public transactionCount;
struct Transaction {
address to;
uint value;
bytes data;
bool executed;
uint confirmations;
mapping(address => bool) confirmedBy;
}
mapping(uint => Transaction) public transactions;
event TransactionSubmitted(uint indexed transactionId, address indexed sender, address to, uint value, bytes data);
event TransactionConfirmed(uint indexed transactionId, address indexed owner);
event TransactionExecuted(uint indexed transactionId, bool success);
event OwnerAdded(address indexed newOwner);
event OwnerRemoved(address indexed oldOwner);
event RequiredUpdated(uint newRequired);
constructor(address[] memory _owners, uint _required) {
require(_owners.length > 0, "At least one owner required");
require(_required > 0 && _required <= _owners.length, "Invalid required number");
for (uint i = 0; i < _owners.length; i++) {
address owner = _owners[i];
require(owner != address(0), "Invalid owner address");
require(!isOwner[owner], "Duplicate owner");
isOwner[owner] = true;
owners.push(owner);
}
required = _required;
}
}
代码分析:
- 状态变量 :
owners
:存储签名者地址的动态数组。required
:需要的签名数(M-of-N)。isOwner
:映射检查地址是否为签名者,方便快速验证。transactionCount
:跟踪交易提案的总数,生成唯一ID。
- 结构体 :
Transaction
:记录每个提案的细节,包括目标地址(to
)、金额(value
)、数据(data
)、是否执行(executed
)、确认数(confirmations
)和确认者(confirmedBy
)。
- 映射 :
transactions
:用交易ID映射到Transaction
结构体,存储所有提案。
- 事件 :
- 定义了提交、确认、执行、添加/移除签名者、更新签名数的イベント,方便前端监听。
- 构造函数 :
- 接受签名者列表和所需签名数。
- 验证:至少有一个签名者,
required
合法,签名者地址有效且不重复。 - 初始化
owners
和isOwner
。
这个框架为多签钱包打下了基础,接下来实现核心功能。
提交交易提案
签名者可以提交交易提案(比如转ETH给某个地址)。我们写一个submitTransaction
函数:
solidity
modifier onlyOwner() {
require(isOwner[msg.sender], "Not an owner");
_;
}
function submitTransaction(address _to, uint _value, bytes memory _data) external onlyOwner {
uint transactionId = transactionCount++;
Transaction storage transaction = transactions[transactionId];
transaction.to = _to;
transaction.value = _value;
transaction.data = _data;
transaction.executed = false;
transaction.confirmations = 0;
emit TransactionSubmitted(transactionId, msg.sender, _to, _value, _data);
}
代码分析:
- 修饰符 :
onlyOwner
确保只有签名者能调用。 - 交易ID :用
transactionCount++
生成唯一ID。 - 存储提案 :在
transactions
映射中初始化Transaction
结构体,记录目标地址、金额和数据(data
支持调用其他合约)。 - 事件 :触发
TransactionSubmitted
,记录提案细节。 - 灵活性 :
data
参数允许提案调用其他合约的函数(比如转ERC20代币)。
确认交易提案
签名者通过confirmTransaction
投票支持提案:
solidity
function confirmTransaction(uint _transactionId) external onlyOwner {
Transaction storage transaction = transactions[_transactionId];
require(!transaction.executed, "Transaction already executed");
require(!transaction.confirmedBy[msg.sender], "Already confirmed");
transaction.confirmedBy[msg.sender] = true;
transaction.confirmations += 1;
emit TransactionConfirmed(_transactionId, msg.sender);
}
代码分析:
- 验证 :
- 确保交易存在(
transactions[_transactionId]
会报错如果ID无效)。 - 确保交易未执行(
!transaction.executed
)。 - 确保调用者未确认(
!transaction.confirmedBy[msg.sender]
)。
- 确保交易存在(
- 更新状态:标记调用者已确认,增加确认数。
- 事件 :触发
TransactionConfirmed
,记录确认者。
执行交易
当确认数达到required
时,执行交易:
solidity
function executeTransaction(uint _transactionId) external onlyOwner {
Transaction storage transaction = transactions[_transactionId];
require(!transaction.executed, "Transaction already executed");
require(transaction.confirmations >= required, "Not enough confirmations");
transaction.executed = true;
(bool success, ) = transaction.to.call{value: transaction.value}(transaction.data);
require(success, "Transaction execution failed");
emit TransactionExecuted(_transactionId, success);
}
代码分析:
- 验证 :
- 确保交易未执行。
- 确保确认数足够(
confirmations >= required
)。
- 执行 :用
call
低级调用执行交易,支持ETH转账或调用其他合约。 - 安全 :检查
success
确保调用成功,失败则回滚。 - 事件 :触发
TransactionExecuted
,记录结果。
注意 :call
比transfer
更灵活(支持动态Gas和调用合约),但需小心重入攻击(我们稍后优化)。
撤销确认
为了灵活性,允许签名者撤销确认(在交易未执行前):
solidity
function revokeConfirmation(uint _transactionId) external onlyOwner {
Transaction storage transaction = transactions[_transactionId];
require(!transaction.executed, "Transaction already executed");
require(transaction.confirmedBy[msg.sender], "Not confirmed by sender");
transaction.confirmedBy[msg.sender] = false;
transaction.confirmations -= 1;
}
分析:
- 验证:确保交易未执行,调用者已确认。
- 更新:撤销确认,减少确认数。
- **用წ
System: 用例:灵活性,允许签名者在执行前改变主意。
完整基础版代码
整合以上代码,得到基础版多签钱包:
solidity
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract MultiSigWallet {
address[] public owners;
uint public required;
mapping(address => bool) public isOwner;
uint public transactionCount;
struct Transaction {
address to;
uint value;
bytes data;
bool executed;
uint confirmations;
mapping(address => bool) confirmedBy;
}
mapping(uint => Transaction) public transactions;
event TransactionSubmitted(uint indexed transactionId, address indexed sender, address to, uint value, bytes data);
event TransactionConfirmed(uint indexed transactionId, address indexed owner);
event Transaction executed(uint indexed transactionId, bool success);
event OwnerAdded(address indexed newOwner);
event OwnerRemoved(address indexed oldOwner);
event RequiredUpdated(uint newRequired);
modifier onlyOwner() {
require(isOwner[msg.sender], "Not an owner");
_;
}
constructor(address[] memory _owners, uint _required) {
require(_owners.length > 0, "At least one owner required");
require(_required > 0 && _required <= _owners.length, "Invalid required number");
for (uint i = 0; i < _owners.length; i++) {
address owner = _owners[i];
require(owner != address(0), "Invalid owner address");
require(!isOwner[owner], "Duplicate owner");
isOwner[owner] = true;
owners.push(owner);
}
required = _required;
}
function submitTransaction(address _to, uint _value, bytes memory _data) external onlyOwner {
uint transactionId = transactionCount++;
Transaction storage transaction = transactions[transactionId];
transaction.to = _to;
transaction.value = _value;
transaction.data = _data;
transaction.executed = false;
transaction.confirmations = 0;
emit TransactionSubmitted(transactionId, msg.sender, _to, _value, _data);
}
function confirmTransaction(uint _transactionId) external onlyOwner {
Transaction storage transaction = transactions[_transactionId];
require(!transaction.executed, "Transaction already executed");
require(!transaction.confirmedBy[msg.sender], "Already confirmed");
transaction.confirmedBy[msg.sender] = true;
transaction.confirmations += 1;
emit TransactionConfirmed(_transactionId, msg.sender);
}
function executeTransaction(uint _transactionId) external onlyOwner {
Transaction storage transaction = transactions[_transactionId];
require(!transaction.executed, "Transaction already executed");
require(transaction.confirmations >= required, "Not enough confirmations");
transaction.executed = true;
(bool success, ) = transaction.to.call{value: transaction.value}(transaction.data);
require(success, "Transaction execution failed");
emit TransactionExecuted(_transactionId, success);
}
function revokeConfirmation(uint _transactionId) external onlyOwner {
Transaction storage transaction = transactions[_transactionId];
require(!transaction.executed, "Transaction already executed");
require(transaction.confirmedBy[msg.sender], "Not confirmed by sender");
transaction.confirmedBy[msg.sender] = false;
transaction.confirmations -= 1;
}
}
这个版本已经功能完整,但还有优化空间,接下来我们会加入高级功能和安全措施。
优化多签钱包
基础版已经能用,但离生产环境还差一些。我们来优化以下方面:
- 权限管理:添加/移除签名者,调整签名数。
- 时间敏感功能:为提案设置超时机制。
- 安全措施:防止重入攻击、优化Gas。
- 用户体验:查询提案和确认状态。
权限管理
允许动态添加/移除签名者和调整签名数(需要多签确认):
solidity
function submitOwnerAddition(address _newOwner) external onlyOwner {
require(_newOwner != address(0), "Invalid owner address");
require(!isOwner[_newOwner], "Already an owner");
uint transactionId = transactionCount++;
Transaction storage transaction = transactions[transactionId];
transaction.to = address(this);
transaction.value = 0;
transaction.data = abi.encodeWithSignature("addOwner(address)", _newOwner);
transaction.executed = false;
transaction.confirmations = 0;
emit TransactionSubmitted(transactionId, msg.sender, address(this), 0, transaction.data);
}
function addOwner(address _newOwner) external {
require(isOwner[msg.sender], "Not an owner");
require(!isOwner[_newOwner], "Already an owner");
isOwner[_newOwner] = true;
owners.push(_newOwner);
emit OwnerAdded(_newOwner);
}
分析:
- 提案机制:添加新签名者需要提交提案并获得足够确认。
- 安全:通过多签流程防止单人恶意添加。
- 类似功能 :移除签名者和更新
required
可以类似实现(略)。
时间敏感功能
为提案添加超时机制,过期后自动失效:
solidity
uint public constant TRANSACTION_TIMEOUT = 7 days;
struct Transaction {
address to;
uint value;
bytes data;
bool executed;
uint confirmations;
uint timestamp;
mapping(address => bool) confirmedBy;
}
function submitTransaction(address _to, uint _value, bytes memory _data) external onlyOwner {
uint transactionId = transactionCount++;
Transaction storage transaction = transactions[transactionId];
transaction.to = _to;
transaction.value = _value;
transaction.data = _data;
transaction.executed = false;
transaction.confirmations = 0;
transaction.timestamp = block.timestamp;
emit TransactionSubmitted(transactionId, msg.sender, _to, _value, _data);
}
function confirmTransaction(uint _transactionId) external onlyOwner {
Transaction storage transaction = transactions[_transactionId];
require(!transaction.executed, "Transaction already executed");
require(block.timestamp <= transaction.timestamp + TRANSACTION_TIMEOUT, "Transaction timed out");
require(!transaction.confirmedBy[msg.sender], "Already confirmed");
transaction.confirmedBy[msg.sender] = true;
transaction.confirmations += 1;
emit TransactionConfirmed(_transactionId, msg.sender);
}
分析:
- 超时机制 :提案创建后7天未执行则失效(
TRANSACTION_TIMEOUT
)。 - 时间戳 :
transaction.timestamp
记录提交时间。 - 防止僵尸提案:避免未决提案长期占用存储。
安全措施
- 防重入攻击 :
executeTransaction
的call
可能触发目标合约的回调,需确保状态更新在调用前完成(已实现)。 - Gas优化:避免循环操作,比如批量确认可以用单独函数优化。
- 权限检查 :所有关键函数使用
onlyOwner
修饰符。 - 紧急停止:可添加暂停功能(需多签确认),防止异常情况。
用户体验
添加查询函数,方便查看提案和确认状态:
solidity
function getTransaction(uint _transactionId) external view returns (
address to,
uint value,
bytes memory data,
bool executed,
uint confirmations,
uint timestamp
) {
Transaction storage transaction = transactions[_transactionId];
return (
transaction.to,
transaction.value,
transaction.data,
transaction.executed,
transaction.confirmations,
transaction.timestamp
);
}
function hasConfirmed(uint _transactionId, address _owner) external view returns (bool) {
return transactions[_transactionId].confirmedBy[_owner];
}
分析:
- 查询提案 :
getTransaction
返回提案详情,方便前端显示。 - 确认状态 :
hasConfirmed
检查某人是否确认过。 - 视图函数:不消耗Gas,适合频繁调用。
完整优化版代码
整合优化后的代码(部分省略重复功能):
solidity
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract MultiSigWallet {
address[] public owners;
uint public required;
mapping(address => bool) public isOwner;
uint public transactionCount;
uint public constant TRANSACTION_TIMEOUT = 7 days;
struct Transaction {
address to;
uint value;
bytes data;
bool executed;
uint confirmations;
uint timestamp;
mapping(address => bool) confirmedBy;
}
mapping(uint => Transaction) public transactions;
event TransactionSubmitted(uint indexed transactionId, address indexed sender, address to, uint value, bytes data);
event TransactionConfirmed(uint indexed transactionId, address indexed owner);
event TransactionExecuted(uint indexed transactionId, bool success);
event OwnerAdded(address indexed newOwner);
modifier onlyOwner() {
require(isOwner[msg.sender], "Not an owner");
_;
}
constructor(address[] memory _owners, uint _required) {
require(_owners.length > 0, "At least one owner required");
require(_required > 0 && _required <= _owners.length, "Invalid required number");
for (uint i = 0; i < _owners.length; i++) {
address owner = _owners[i];
require(owner != address(0), "Invalid owner address");
require(!isOwner[owner], "Duplicate owner");
isOwner[owner] = true;
owners.push(owner);
}
required = _required;
}
function submitTransaction(address _to, uint _value, bytes memory _data) external onlyOwner {
uint transactionId = transactionCount++;
Transaction storage transaction = transactions[transactionId];
transaction.to = _to;
transaction.value = _value;
transaction.data = _data;
transaction.executed = false;
transaction.confirmations = 0;
transaction.timestamp = block.timestamp;
emit TransactionSubmitted(transactionId, msg.sender, _to, _value, _data);
}
function confirmTransaction(uint _transactionId) external onlyOwner {
Transaction storage transaction = transactions[_transactionId];
require(!transaction.executed, "Transaction already executed");
require(block.timestamp <= transaction.timestamp + TRANSACTION_TIMEOUT, "Transaction timed out");
require(!transaction.confirmedBy[msg.sender], "Already confirmed");
transaction.confirmedBy[msg.sender] = true;
transaction.confirmations += 1;
emit TransactionConfirmed(_transactionId, msg.sender);
}
function executeTransaction(uint _transactionId) external onlyOwner {
Transaction storage transaction = transactions[_transactionId];
require(!transaction.executed, "Transaction already executed");
require(transaction.confirmations >= required, "Not enough confirmations");
transaction.executed = true;
(bool success, ) = transaction.to.call{value: transaction.value}(transaction.data);
require(success, "Transaction execution failed");
emit TransactionExecuted(_transactionId, success);
}
function revokeConfirmation(uint _transactionId) external onlyOwner {
Transaction storage transaction = transactions[_transactionId];
require(!transaction.executed, "Transaction already executed");
require(transaction.confirmedBy[msg.sender], "Not confirmed by sender");
transaction.confirmedBy[msg.sender] = false;
transaction.confirmations -= 1;
}
function submitOwnerAddition(address _newOwner) external onlyOwner {
require(_newOwner != address(0), "Invalid owner address");
require(!isOwner[_newOwner], "Already an owner");
uint transactionId = transactionCount++;
Transaction storage transaction = transactions[transactionId];
transaction.to = address(this);
transaction.value = 0;
transaction.data = abi.encodeWithSignature("addOwner(address)", _newOwner);
transaction.executed = false;
transaction.confirmations = 0;
transaction.timestamp = block.timestamp;
emit TransactionSubmitted(transactionId, msg.sender, address(this), 0, transaction.data);
}
function addOwner(address _newOwner) external {
require(isOwner[msg.sender], "Not an owner");
require(!isOwner[_newOwner], "Already an owner");
isOwner[_newOwner] = true;
owners.push(_newOwner);
emit OwnerAdded(_newOwner);
}
function getTransaction(uint _transactionId) external view returns (
address to,
uint value,
bytes memory data,
bool executed,
uint confirmations,
uint timestamp
) {
Transaction storage transaction = transactions[_transactionId];
return (
transaction.to,
transaction.value,
transaction.data,
transaction.executed,
transaction.confirmations,
transaction.timestamp
);
}
function hasConfirmed(uint _transactionId, address _owner) external view returns (bool) {
return transactions[_transactionId].confirmedBy[_owner];
}
receive() external payable {}
}
分析:
- 完整功能:支持提案、确认、执行、撤销、权限管理和查询。
- 安全性:防重入、超时机制、严格验证。
- 用户体验:事件和查询函数便于前端集成。
- Gas优化:避免复杂循环,状态更新高效。
进阶功能:支持ERC20代币
多签钱包不仅能管理ETH,还能管理ERC20代币。我们通过data
字段调用ERC20的transfer
函数:
solidity
function submitERC20Transfer(address _token, address _to, uint _amount) external onlyOwner {
uint transactionId = transactionCount++;
Transaction storage transaction = transactions[transactionId];
transaction.to = _token;
transaction.value = 0;
transaction.data = abi.encodeWithSignature("transfer(address,uint256)", _to, _amount);
transaction.executed = false;
transaction.confirmations = 0;
transaction.timestamp = block.timestamp;
emit TransactionSubmitted(transactionId, msg.sender, _token, 0, transaction.data);
}
分析:
- ERC20支持 :通过
abi.encodeWithSignature
生成transfer
调用的数据。 - 通用性 :
data
字段支持任何ERC20代币的转账。 - 安全:确保目标合约是可信的ERC20合约(需外部验证)。
踩坑经验
常见错误
- 无效签名者:构造函数未检查重复或无效地址(已修复)。
- 提案僵尸化:没有超时机制导致未决提案占用存储(已加超时)。
- 重入攻击 :
call
未正确处理回调风险(状态先更新)。 - Gas超限:大量签名者可能导致查询或操作Gas过高(优化存储结构)。
实践应用
- 最小签名者 :保持
owners
数量合理(比如3-5个),避免Gas成本过高。 - 事件记录:为所有关键操作触发事件,方便跟踪。
- 超时机制:为提案设置合理超时(7天较常见)。
- 测试充分:用Hardhat测试所有场景(提案、确认、执行、超时、权限)。
- 文档清晰:注释说明每个函数的用途和限制。
- 升级机制:通过多签支持合约升级(需额外实现)。
实际应用场景
多签钱包在以下场景广泛应用:
- DAO国库:管理社区资金,如Aragon或Moloch DAO。
- 团队资金:开发团队或公司管理加密资产。
- 托管交易:买卖双方用多签确保资金安全。
- DeFi协议:管理协议的控制权或紧急暂停。
- NFT管理:多人共同控制稀有NFT的转移。
以Gnosis Safe为例,它是多签钱包的标杆,支持复杂权限、模块化扩展和用户友好的前端。