前言

在区块链系统中,私钥代表资产的最终控制权。传统单签名(EOA)账户存在典型的单点失效风险:一旦私钥被盗、丢失或遭胁迫,整个账户资产即面临永久性损失。

多重签名(Multi-Signature,简称多签)机制通过将交易执行权限分散至多个独立密钥,并要求达到预设阈值(M-of-N)才能生效,从根本上消除了单点风险。即使部分密钥失控,只要未满足阈值,攻击者仍无法单方面转移资产。

典型阈值配置:

  • 2-of-3:3 人管钱包,任何 2 人同意就能转账
  • 4-of-7:7 人理事会,超过半数即可执行,适合 DAO 金库

应用场景:

  • 团队/公司资金:杜绝一人独断、防止内鬼
  • DAO 金库:所有大额支出必须集体投票
  • 交易所冷钱包:行业标配,99% 的头部交易所都在用多签
  • 个人高净值资产:把硬件钱包 + 亲人 + 律师组合成 3-of-5,永不失控

在 EVM 生态中,多签功能通常通过智能合约账户(Smart Contract Account)实现,其中 Gnosis Safe(现更名为 Safe{Wallet})是最为主流的生产级方案。截至 2025 年,其单一合约体系保护的链上资产总值已超过 400 亿美元,经过多次第三方审计与长期实战验证,成为事实上的行业标准。

接下来,我们用不到 200 行代码,深入剖析一个简洁、透明、可直接用于生产的多签钱包合约实现,完整展示核心机制、状态管理、安全设计与最佳实践。

代码拆解

1. 事件定义

这是多签钱包操作的关键日志记录:

 1// 记录存款事件,包含发送者地址、存款金额和合约余额
 2event Deposit(address indexed sender, uint amount, uint balance);
 3// 记录提交交易事件,包含交易索引、提交者地址、目标地址、转账金额和调用数据
 4event SubmitTransaction(
 5    uint indexed txIndex,
 6    address indexed owner,
 7    address indexed to,
 8    uint value,
 9    bytes data
10);
11// 记录确认交易事件,包含交易索引和确认者地址
12event ConfirmTransaction(uint indexed txIndex, address indexed owner);
13// 记录撤销确认事件,包含交易索引和撤销者地址
14event RevokeConfirmation(uint indexed txIndex, address indexed owner);
15// 记录执行交易事件,包含交易索引、目标地址、转账金额和调用数据
16event ExecuteTransaction(
17    uint indexed txIndex,
18    address indexed to,
19    uint value,
20    bytes data
21);

这些事件通过indexed关键字优化了日志查询效率,便于前端应用监听和处理合约状态变化。

2. 状态变量定义

 1// 多签持有人地址列表
 2address[] public owners;
 3// 记录地址是否为多签持有人
 4mapping(address => bool) public isOwner;
 5// 执行交易所需的最小确认数
 6uint public numConfirmationsRequired;
 7
 8// 交易结构体,记录交易的详细信息
 9struct Transaction {
10    address to;      // 目标地址
11    uint value;      // 转账金额
12    bytes data;      // 调用数据
13    bool executed;   // 是否已执行
14    uint numConfirmations;  // 已获得的确认数
15}
16
17// 记录交易的确认状态,mapping: 交易索引 => 持有人地址 => 是否确认
18mapping(uint => mapping(address => bool)) public isConfirmed;
19
20// 所有交易列表
21Transaction[] public transactions;

这里定义了合约的核心数据结构:

  • owners数组存储多签持有人地址
  • isOwner映射快速验证地址是否为多签持有人
  • numConfirmationsRequired定义执行交易所需的最小确认数
  • Transaction结构体记录每笔交易的详细信息
  • isConfirmed双重映射记录每笔交易的确认状态
  • transactions数组存储所有交易记录

3. 访问控制修饰符

 1// 限制只有多签持有人可以调用
 2modifier onlyOwner() {
 3    require(isOwner[msg.sender], "not owner");
 4    _;
 5}
 6
 7// 验证交易是否存在
 8modifier txExists(uint _txIndex) {
 9    require(_txIndex < transactions.length, "tx does not exist");
10    _;
11}
12
13// 验证交易是否未执行
14modifier notExecuted(uint _txIndex) {
15    require(!transactions[_txIndex].executed, "tx already executed");
16    _;
17}
18
19// 验证交易是否未被当前调用者确认
20modifier notConfirmed(uint _txIndex) {
21    require(!isConfirmed[_txIndex][msg.sender], "tx already confirmed");
22    _;
23}

这些修饰符提供了关键的安全检查:

  • onlyOwner确保只有多签持有人可以执行敏感操作
  • txExists防止访问不存在的交易索引
  • notExecuted防止重复执行已执行的交易
  • notConfirmed防止重复确认

4. 构造函数

 1// @notice 构造函数,初始化多签持有人列表和所需确认数
 2// @param _owners 多签持有人地址列表
 3// @param _numConfirmationsRequired 所需确认数
 4constructor(address[] memory _owners, uint _numConfirmationsRequired) {
 5    require(_owners.length > 0, "owners required");
 6    require(
 7        _numConfirmationsRequired > 0 &&
 8            _numConfirmationsRequired <= _owners.length,
 9        "invalid number of required confirmations"
10    );
11
12    // 初始化多签持有人
13    for (uint i = 0; i < _owners.length; i++) {
14        address owner = _owners[i];
15
16        require(owner != address(0), "invalid owner");
17        require(!isOwner[owner], "owner not unique");
18
19        isOwner[owner] = true;
20        owners.push(owner);
21    }
22
23    numConfirmationsRequired = _numConfirmationsRequired;
24}

构造函数执行了必要的初始化和验证:

  • 验证多签持有人数组非空
  • 验证确认数在合理范围内(大于0且不超过持有人数量)
  • 遍历验证并添加每个持有人
  • 验证持有人地址有效性
  • 设置确认数阈值
flowchart TD
    A[开始] --> B{所有者数组长度 > 0?}
    B-->|否| C[抛出错误: owners required]
    B-->|是| D{所需确认数 > 0 且 ≤ 所有者数量?}
    D-->|否| E[抛出错误: invalid number of required confirmations]
    D-->|是| F[遍历所有者数组]
    F --> G{当前所有者地址 ≠ 0 且未重复?}
    G-->|否| H[抛出错误: invalid owner / owner not unique]
    G-->|是| I[设置 isOwner = true]
    I --> J[将 owner 加入 owners 数组]
    J --> K{还有下一个所有者?}
    K-->|是| F
    K-->|否| L[保存 numConfirmationsRequired]
    L --> M[构造函数结束]

5. 存款功能

1// @notice 接收ETH的回调函数
2receive() external payable {
3    emit Deposit(msg.sender, msg.value, address(this).balance);
4}

receive函数允许合约接收ETH转账,同时触发存款事件记录相关信息。

6. 交易提交功能

 1// @notice 提交新的交易提案
 2// @param _to 目标地址
 3// @param _value 转账金额
 4// @param _data 调用数据
 5function submitTransaction(
 6    address _to,
 7    uint _value,
 8    bytes memory _data
 9) public onlyOwner {
10    uint txIndex = transactions.length;
11
12    transactions.push(
13        Transaction({
14            to: _to,
15            value: _value,
16            data: _data,
17            executed: false,
18            numConfirmations: 0
19        })
20    );
21
22    emit SubmitTransaction(txIndex, msg.sender, _to, _value, _data);
23}

提交交易功能允许多签持有人发起新的交易提案,创建新的交易记录并触发事件。这个函数很简单,就是把一个新的交易添加到数组中,初始状态是未确认、未执行。

7. 交易确认功能

 1// @notice 确认交易
 2// @param _txIndex 交易索引
 3function confirmTransaction(uint _txIndex)
 4    public
 5    onlyOwner
 6    txExists(_txIndex)
 7    notExecuted(_txIndex)
 8    notConfirmed(_txIndex)
 9{
10    Transaction storage transaction = transactions[_txIndex];
11    transaction.numConfirmations += 1;
12    isConfirmed[_txIndex][msg.sender] = true;
13
14    emit ConfirmTransaction(_txIndex, msg.sender);
15}

确认交易功能执行了多项验证,确保操作的安全性,并更新交易确认状态。这个函数比较关键,它会增加交易的确认数,并更新当前用户对该交易的确认状态。

8. 交易执行功能

 1// @notice 执行已获得足够确认数的交易
 2// @param _txIndex 交易索引
 3function executeTransaction(uint _txIndex)
 4    public
 5    txExists(_txIndex)
 6    notExecuted(_txIndex)
 7{
 8    Transaction storage transaction = transactions[_txIndex];
 9
10    require(
11        transaction.numConfirmations >= numConfirmationsRequired,
12        "cannot execute tx"
13    );
14
15    transaction.executed = true;
16
17    (bool success, ) = transaction.to.call{value: transaction.value}(
18        transaction.data
19    );
20
21    require(success, "tx failed");
22
23    emit ExecuteTransaction(
24        _txIndex,
25        transaction.to,
26        transaction.value,
27        transaction.data
28    );
29}

执行交易功能检查确认数是否达到阈值,然后通过低级call执行交易,并确保执行成功。这里使用了低级调用call,可以处理各种类型的交易(转账或调用合约)。

9. 确认撤销功能

 1// @notice 撤销对交易的确认
 2// @param _txIndex 交易索引
 3function revokeConfirmation(uint _txIndex)
 4    public
 5    onlyOwner
 6    txExists(_txIndex)
 7    notExecuted(_txIndex)
 8{
 9    require(isConfirmed[_txIndex][msg.sender], "tx not confirmed");
10
11    Transaction storage transaction = transactions[_txIndex];
12    transaction.numConfirmations -= 1;
13    isConfirmed[_txIndex][msg.sender] = false;
14
15    emit RevokeConfirmation(_txIndex, msg.sender);
16}

撤销确认功能允许持有人撤销他们之前的确认,更新交易状态。万一用户误确认了一笔交易,可以撤销。

10. 查询辅助函数

 1function getOwners() public view returns (address[] memory) {
 2    return owners;
 3}
 4
 5function getTransactionCount() public view returns (uint) {
 6    return transactions.length;
 7}
 8
 9function getTransaction(uint _txIndex)
10    public
11    view
12    returns (
13        address to,
14        uint value,
15        bytes memory data,
16        bool executed,
17        uint numConfirmations
18    )
19{
20    Transaction storage transaction = transactions[_txIndex];
21
22    return (
23        transaction.to,
24        transaction.value,
25        transaction.data,
26        transaction.executed,
27        transaction.numConfirmations
28    );
29}

这些辅助函数提供了对外查询合约状态的接口,便于前端应用展示多签钱包的当前状态。

交易流程图

sequenceDiagram
    participant Owner1 as 多签持有人1
    participant Owner2 as 多签持有人2
    participant Owner3 as 多签持有人3
    participant Contract as 多签合约

    Owner1->>Contract: submitTransaction(提交交易)
    Note over Contract: 交易状态: 未确认(0/2)

    Owner2->>Contract: confirmTransaction(确认交易)
    Note over Contract: 交易状态: 已确认(1/2)

    Owner3->>Contract: confirmTransaction(确认交易)
    Note over Contract: 交易状态: 已确认(2/2)

    Owner1->>Contract: executeTransaction(执行交易)
    Note over Contract: 交易状态: 已执行

合约安全

在实际开发中,需要特别关注几个安全点:

  1. 访问控制:通过onlyOwner修饰符确保只有多签持有人可以执行操作,避免外部攻击者调用敏感函数。
  2. 参数验证:在构造函数中验证输入参数的合理性(如持有人数量、确认阈值),防止无效配置导致合约锁定或漏洞。
  3. 状态检查:在执行操作前验证交易状态(存在性、执行状态、确认状态),防止重复操作或无效调用。
  4. 重入保护:在executeTransaction中,先更新executed状态再进行外部call,避免重入攻击(reentrancy),符合 Checks-Effects-Interactions 模式。
  5. 事件记录:所有关键操作(如提交、确认、执行)都触发事件,便于链上追踪和前端监听。
  6. Gas 效率:使用storage引用避免不必要拷贝;owners数组只在初始化时填充,后续用mapping快速查询。

Gas优化

  1. 使用indexed参数:在事件中使用indexed参数优化查询效率,降低前端监听成本(例如通过 The Graph 或 Etherscan 过滤)。
  2. 存储优化:使用mappingstorage关键字优化存储访问,减少Gas消耗;避免大数组操作。
  3. 循环优化:在构造函数中使用简单的for循环初始化数据,避免复杂计算;实际部署时,持有人数量控制在5-10以内以防Gas超限。
  4. 批量处理:可扩展支持批量确认(e.g., confirmTransactions(uint[] calldata txIndices)),节省多次交易Gas。
  5. 低级调用calltransfer更灵活,支持合约交互,但需检查返回值防失败。

潜在风险

  1. 重入攻击:虽然已更新状态先于调用,但若外部合约回调恶意,需额外用 ReentrancyGuard(如 OpenZeppelin)。
  2. Gas限制:大量持有人的确认操作可能消耗过多Gas,特别是在交易执行时;建议阈值不超过5。
  3. 密钥管理:如果多签持有人无法联系,可能影响紧急交易的执行;可集成时间锁模块允许超时自动执行。
  4. 前端依赖:事件监听需可靠后端(如 The Graph),否则用户可能错过状态更新。
  5. 升级性:合约不可升级,部署前需审计;推荐用 Gnosis Safe 的模块化设计替代自定义实现。
  6. 链上兼容:EVM链Gas费波动大,测试网(如 Sepolia)模拟高Gas场景。

完整代码

  1// SPDX-License-Identifier: MIT
  2pragma solidity ^0.8.20;
  3
  4// @title 多签钱包合约
  5// @notice 这是一个支持多人签名的钱包合约,可以用于团队资金管理
  6contract ContractWallet {
  7    // 记录存款事件,包含发送者地址、存款金额和合约余额
  8    event Deposit(address indexed sender, uint amount, uint balance);
  9    // 记录提交交易事件,包含交易索引、提交者地址、目标地址、转账金额和调用数据
 10    event SubmitTransaction(
 11        uint indexed txIndex,
 12        address indexed owner,
 13        address indexed to,
 14        uint value,
 15        bytes data
 16    );
 17    // 记录确认交易事件,包含交易索引和确认者地址
 18    event ConfirmTransaction(uint indexed txIndex, address indexed owner);
 19    // 记录撤销确认事件,包含交易索引和撤销者地址
 20    event RevokeConfirmation(uint indexed txIndex, address indexed owner);
 21    // 记录执行交易事件,包含交易索引、目标地址、转账金额和调用数据
 22    event ExecuteTransaction(
 23        uint indexed txIndex,
 24        address indexed to,
 25        uint value,
 26        bytes data
 27    );
 28
 29    // 多签持有人地址列表
 30    address[] public owners;
 31    // 记录地址是否为多签持有人
 32    mapping(address => bool) public isOwner;
 33    // 执行交易所需的最小确认数
 34    uint public numConfirmationsRequired;
 35
 36    // 交易结构体,记录交易的详细信息
 37    struct Transaction {
 38        address to;      // 目标地址
 39        uint value;      // 转账金额
 40        bytes data;      // 调用数据
 41        bool executed;   // 是否已执行
 42        uint numConfirmations;  // 已获得的确认数
 43    }
 44
 45    // 记录交易的确认状态,mapping: 交易索引 => 持有人地址 => 是否确认
 46    mapping(uint => mapping(address => bool)) public isConfirmed;
 47
 48    // 所有交易列表
 49    Transaction[] public transactions;
 50
 51    // 限制只有多签持有人可以调用
 52    modifier onlyOwner() {
 53        require(isOwner[msg.sender], "not owner");
 54        _;
 55    }
 56
 57    // 验证交易是否存在
 58    modifier txExists(uint _txIndex) {
 59        require(_txIndex < transactions.length, "tx does not exist");
 60        _;
 61    }
 62
 63    // 验证交易是否未执行
 64    modifier notExecuted(uint _txIndex) {
 65        require(!transactions[_txIndex].executed, "tx already executed");
 66        _;
 67    }
 68
 69    // 验证交易是否未被当前调用者确认
 70    modifier notConfirmed(uint _txIndex) {
 71        require(!isConfirmed[_txIndex][msg.sender], "tx already confirmed");
 72        _;
 73    }
 74
 75    // @notice 构造函数,初始化多签持有人列表和所需确认数
 76    // @param _owners 多签持有人地址列表
 77    // @param _numConfirmationsRequired 所需确认数
 78    constructor(address[] memory _owners, uint _numConfirmationsRequired) {
 79        require(_owners.length > 0, "owners required");
 80        require(
 81            _numConfirmationsRequired > 0 &&
 82                _numConfirmationsRequired <= _owners.length,
 83            "invalid number of required confirmations"
 84        );
 85
 86        // 初始化多签持有人
 87        for (uint i = 0; i < _owners.length; i++) {
 88            address owner = _owners[i];
 89
 90            require(owner != address(0), "invalid owner");
 91            require(!isOwner[owner], "owner not unique");
 92
 93            isOwner[owner] = true;
 94            owners.push(owner);
 95        }
 96
 97        numConfirmationsRequired = _numConfirmationsRequired;
 98    }
 99
100    // @notice 接收ETH的回调函数
101    receive() external payable {
102        emit Deposit(msg.sender, msg.value, address(this).balance);
103    }
104
105    // @notice 提交新的交易提案
106    // @param _to 目标地址
107    // @param _value 转账金额
108    // @param _data 调用数据
109    function submitTransaction(
110        address _to,
111        uint _value,
112        bytes memory _data
113    ) public onlyOwner {
114        uint txIndex = transactions.length;
115
116        transactions.push(
117            Transaction({
118                to: _to,
119                value: _value,
120                data: _data,
121                executed: false,
122                numConfirmations: 0
123            })
124        );
125
126        emit SubmitTransaction(txIndex, msg.sender, _to, _value, _data);
127    }
128
129    // @notice 确认交易
130    // @param _txIndex 交易索引
131    function confirmTransaction(uint _txIndex)
132        public
133        onlyOwner
134        txExists(_txIndex)
135        notExecuted(_txIndex)
136        notConfirmed(_txIndex)
137    {
138        Transaction storage transaction = transactions[_txIndex];
139        transaction.numConfirmations += 1;
140        isConfirmed[_txIndex][msg.sender] = true;
141
142        emit ConfirmTransaction(_txIndex, msg.sender);
143    }
144
145    // @notice 执行已获得足够确认数的交易
146    // @param _txIndex 交易索引
147    function executeTransaction(uint _txIndex)
148        public
149        txExists(_txIndex)
150        notExecuted(_txIndex)
151    {
152        Transaction storage transaction = transactions[_txIndex];
153
154        require(
155            transaction.numConfirmations >= numConfirmationsRequired,
156            "cannot execute tx"
157        );
158
159        transaction.executed = true;
160
161        (bool success, ) = transaction.to.call{value: transaction.value}(
162            transaction.data
163        );
164        
165        require(success, "tx failed");
166
167        emit ExecuteTransaction(
168            _txIndex,
169            transaction.to,
170            transaction.value,
171            transaction.data
172        );
173    }
174
175    // @notice 撤销对交易的确认
176    // @param _txIndex 交易索引
177    function revokeConfirmation(uint _txIndex)
178        public
179        onlyOwner
180        txExists(_txIndex)
181        notExecuted(_txIndex)
182    {
183        require(isConfirmed[_txIndex][msg.sender], "tx not confirmed");
184
185        Transaction storage transaction = transactions[_txIndex];
186        transaction.numConfirmations -= 1;
187        isConfirmed[_txIndex][msg.sender] = false;
188
189        emit RevokeConfirmation(_txIndex, msg.sender);
190    }
191
192    // @notice 获取所有多签持有人地址
193    function getOwners() public view returns (address[] memory) {
194        return owners;
195    }
196
197    // @notice 获取交易总数
198    function getTransactionCount() public view returns (uint) {
199        return transactions.length;
200    }
201
202    // @notice 获取交易详情
203    // @param _txIndex 交易索引
204    // @return to 目标地址
205    // @return value 转账金额
206    // @return data 调用数据
207    // @return executed 是否已执行
208    // @return numConfirmations 已获得的确认数
209    function getTransaction(uint _txIndex)
210        public
211        view
212        returns (
213            address to,
214            uint value,
215            bytes memory data,
216            bool executed,
217            uint numConfirmations
218        )
219    {
220        Transaction storage transaction = transactions[_txIndex];
221
222        return (
223            transaction.to,
224            transaction.value,
225            transaction.data,
226            transaction.executed,
227            transaction.numConfirmations
228        );
229    }
230}

总结

本文通过不到 200 行核心代码,完整呈现了一个经典多签钱包实现。该合约作为教学与二次开发的理想起点,已包含多签机制的全部核心要素:

  • 灵活的 M-of-N 阈值配置
  • 防止重复确认/执行的安全修饰符
  • 完整的交易生命周期管理(提交 → 确认 → 执行 → 撤销)
  • 完善的事件系统,便于链下索引与前端集成

通过这种设计,我们可以实现多方共同管理资产,有效提升资金安全性,防止单点故障风险。在实际应用中,建议根据具体需求进行适当的定制和扩展。