深入分析在Solidity中实现多签钱包

今天我们要聊一个在区块链开发中超级重要且实用的主题——多签钱包(Multi-SignatureWallet)。如果你玩过DeFi、DAO或者团队管理的加密资产,肯定听说过多签钱包。它就像一个“多人保险箱”,需要多个签名者同意才能动用资金,极大地提高了安全性和去中心化特性。多签钱包是什么?为什么需

今天我们要聊一个在区块链开发中超级重要且实用的主题——多签钱包(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)。
  • 签名者确认提案,达到所需签名数后执行。
  • 查询提案状态和历史。

基础合约结构

先来看合约的框架,包含核心状态变量和初始化逻辑:

// 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合法,签名者地址有效且不重复。
    • 初始化ownersisOwner

这个框架为多签钱包打下了基础,接下来实现核心功能。

提交交易提案

签名者可以提交交易提案(比如转ETH给某个地址)。我们写一个submitTransaction函数:

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投票支持提案:

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时,执行交易:

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,记录结果。

注意calltransfer更灵活(支持动态Gas和调用合约),但需小心重入攻击(我们稍后优化)。

撤销确认

为了灵活性,允许签名者撤销确认(在交易未执行前):

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: 用例:灵活性,允许签名者在执行前改变主意。

完整基础版代码

整合以上代码,得到基础版多签钱包:

// 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。
  • 用户体验:查询提案和确认状态。

权限管理

允许动态添加/移除签名者和调整签名数(需要多签确认):

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可以类似实现(略)。

时间敏感功能

为提案添加超时机制,过期后自动失效:

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记录提交时间。
  • 防止僵尸提案:避免未决提案长期占用存储。

安全措施

  • 防重入攻击executeTransactioncall可能触发目标合约的回调,需确保状态更新在调用前完成(已实现)。
  • Gas优化:避免循环操作,比如批量确认可以用单独函数优化。
  • 权限检查:所有关键函数使用onlyOwner修饰符。
  • 紧急停止:可添加暂停功能(需多签确认),防止异常情况。

用户体验

添加查询函数,方便查看提案和确认状态:

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,适合频繁调用。

完整优化版代码

整合优化后的代码(部分省略重复功能):

// 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函数:

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为例,它是多签钱包的标杆,支持复杂权限、模块化扩展和用户友好的前端。

点赞 0
收藏 0
分享
本文参与登链社区写作激励计划 ,好文好收益,欢迎正在阅读的你也加入。

0 条评论

请先 登录 后评论
天涯学馆
天涯学馆
0x9d6d...50d5
资深大厂程序员,12年开发经验,致力于探索前沿技术!