多重签名钱包智能合约的实现和应用

Technical Blog1years go (2023)发布 Dexnav
0

多重签名钱包Smart Contracts的实现和应用

电报学习交流:DexDao

多重签名钱包是一种需要多个授权签名才能进行操作的钱包,相对于普通钱包,多签钱包的安全性更高。如果其中一个签名者失去了私钥,其他签名者仍然可以控制钱包和资金。因此,多重签名钱包通常被认为比硬件钱包更安全。

但是,许多人并不知道多重签名钱包的原理是什么以及如何开发多重签名钱包智能Contracts。在这篇文章中,我们将解释多重签名钱包的工作原理,并分享相关的合约代码。

一、创建多签钱包合约

创建多重签名钱包合约有以下步骤:

  1. 设定签名者和门槛(链上):在部署多重签名钱包合约时,我们需要初始化签名者列表和执行门槛(即必须有至少n个签名者授权后,才能执行交易)。虽然Gnosis Safe 多重签名钱包支持增加/删除签名者和改变门槛,但在本篇文章中我们只考虑基本功能。
  2. 创建交易(链下):一笔待授权的交易包含以下内容:
  • to:目标合约。
  • value:交易发送的以太币数量。
  • data:calldata,包含调用函数的选择器和参数。
  • nonce:初始值为0,随着每次多重签名钱包成功执行交易递增,可以防止签名重放攻击。
  • chainid:链 id,防止不同链的签名重放攻击。
  1. 收集多个签名(链下):将上一步的交易 ABI 编码并计算哈希,得到交易哈希。然后,让每个签名者签署,并将它们连接在一起,得到打包签名。如果对ABI编码和哈希不熟悉,请参考WTF Solidity 极简教程第 27 讲和第 28 讲。
  • 交易哈希:0xc1b055cf8e78338db21407b425114a2e258b0318879327945b661bfdea570e66
  • 签名者A的签名:0xd6a56c718fc16f283512f90e16f2e62f888780a712d15e884e300c51e5b100de2f014ad71bcb6d97946ef0d31346b3b71eb688831abedaf41b33486b416129031c
  • 签名者B的签名:0x2184f70a17f14426865bda8ebe391508b8e3984d16ce6d90905ae8beae7d75fd435a7e51d837881d820414ebaf0ff16074204c75b33d66928edcf8dd398249861b
  • 打包签名:0xd6a56c718fc16f283512f90e16f2e62f888780a712d15e884e300c51e5b100de2f014ad71bcb6d97946ef0d31346b3b71eb688831abedaf41b33486b416129031c2184f70a17f14426865bda8ebe391508b8e3984d16ce6d90905ae8beae7d75fd435a7e51d837881d820414ebaf0ff16074204c75b33d66928edcf8dd398249861b
    1. 调用多重签名钱包合约的执行函数,验证签名并执行交易(链上)。

    在执行交易前,多重签名钱包合约会验证签名是否合法。如果签名数达到门槛,则交易被执行,否则交易被拒绝。验证签名的步骤如下:

    • 对交易数据进行哈希,并将哈希与签名一起发送到合约。
    • 合约检查签名是否有效,以及签名者是否在签名者列表中。
    • 如果签名数达到门槛,则合约执行交易,否则交易被拒绝。

    如果验证成功,交易就被执行,否则将不执行。

    这是多重签名钱包合约的基本实现过程。在实际应用中,我们需要考虑更多的安全性和实用性问题,例如钱包的访问控制、签名者管理、门槛调整等。但是这个基本过程可以让我们更好地理解多重签名钱包的工作原理。

    多重签名钱包是一种需要多个授权签名才能进行操作的钱包,具有更高的安全性。多重签名钱包合约的实现需要设定签名者列表和门槛,并且包括交易创建、签名收集和验证等步骤。实现一个多重签名钱包合约需要考虑更多的安全和实用性问题,但是这个基本过程可以帮助我们更好地理解多重签名钱包的工作原理。

二、事件分析

MultisigWallet 合约包括两个事件,ExecutionSuccess 和 ExecutionFailure,分别在交易成功和失败时触发,并传递交易哈希作为参数。

event ExecutionSuccess(bytes32 txHash);    // 交易成功事件
event ExecutionFailure(bytes32 txHash);    // 交易失败事件

 

三、状态变量

MultisigWallet 合约还有五个状态变量。其中 owners 是一个多签持有人的数组,isOwner 是一个 address => bool 的映射,用于记录一个地址是否为多签持有人。ownerCount 是多签持有人的数量,threshold 是多签执行门槛,即交易至少需要 n 个多签持有人的签名才能被执行。最后,nonce 初始值为 0,每次多签合约成功执行交易后递增,可以防止签名重放攻击。

MultisigWallet 合约的状态变量如下:

address[] public owners;                   // 多签持有人数组
mapping(address => bool) public isOwner;   // 记录一个地址是否为多签持有人
uint256 public ownerCount;                 // 多签持有人数量
uint256 public threshold;                  // 多签执行门槛,交易至少有n个多签人签名才能被执行
uint256 public nonce;                      // nonce,防止签名重放攻击

四、函数编写

MultisigWallet合约包括了以下6个函数:

1、构造函数:在合约部署时调用,初始化 owners, isOwner, ownerCount,threshold 状态变量。

构造函数,初始化owners, isOwner, ownerCount, threshold 
constructor(        
    address[] memory _owners,
    uint256 _threshold
) {
    _setupOwners(_owners, _threshold);
}

2、_setupOwners():初始化 owners, isOwner, ownerCount, threshold 状态变量,要求多签执行门槛不能小于1且不能大于多签人数,多签地址不能为0地址且不能重复。

/// @dev 初始化owners, isOwner, ownerCount,threshold 
/// @param _owners: 多签持有人数组
/// @param _threshold: 多签执行门槛,至少有几个多签人签署了交易
function _setupOwners(address[] memory _owners, uint256 _threshold) internal {
    // threshold没被初始化过
    require(threshold == 0, "WTF5000");
    // 多签执行门槛 小于 多签人数
    require(_threshold <= _owners.length, "WTF5001");
    // 多签执行门槛至少为1
    require(_threshold >= 1, "WTF5002");
    
for (uint256 i = 0; i < _owners.length; i++) {
    address owner = _owners[i];
    // 多签人不能为0地址,本合约地址,不能重复
    require(owner != address(0) && owner != address(this) && !isOwner[owner], "WTF5003");
    owners.push(owner);
    isOwner[owner] = true;
}
ownerCount = _owners.length;
threshold = _threshold;

3、execTransaction():在收集足够的多签签名后,验证签名并执行交易。传入的参数为目标地址 to,发送的Ether数额 value,数据 data,以及打包签名 signatures。打包签名是将收集的多签人对交易哈希的签名,按多签持有人地址从小到大顺序,打包到一个[bytes]数据中。这一步调用了 encodeTransactionData()编码交易,调用了 checkSignatures()检验签名是否有效、数量是否达到执行门槛。

/// @dev 在收集足够的多签签名后,执行交易
/// @param to 目标合约地址
/// @param value msg.value,支付的以太坊
/// @param data calldata
/// @param signatures 打包的签名,对应的多签地址由小到达,方便检查。 ({bytes32 r}{bytes32 s}{uint8 v}) (第一个多签的签名, 第二个多签的签名 ... )
function execTransaction(
    address to,
    uint256 value,
    bytes memory data,
    bytes memory signatures
) public payable virtual returns (bool success) {
    // 编码交易数据,计算哈希
    bytes32 txHash = encodeTransactionData(to, value, data, nonce, block.chainid);
    nonce++;  // 增加nonce
    checkSignatures(txHash, signatures); // 检查签名
    // 利用call执行交易,并获取交易结果
    (success, ) = to.call{value: value}(data);
    require(success , "WTF5004");
    if (success) emit ExecutionSuccess(txHash);
    else emit ExecutionFailure(txHash);

 

4、checkSignatures():检查签名和交易数据的哈希是否对应,数量是否达到门槛,若否,交易会 revert。单个签名长度为 65 字节,因此打包签名的长度要长于 threshold * 65。调用了 signatureSplit()分离出单个签名。

  • 用 ecdsa 获取签名地址.

  • 利用 currentOwner > lastOwner 确定签名来自不同多签(多签地址递增)

  • 利用 isOwner[currentOwner]确定签名者为多签持有人。

/**
 * @dev 检查签名和交易数据是否对应。如果是无效签名,交易会revert
 * @param dataHash 交易数据哈希
 * @param signatures 几个多签签名打包在一起
 */
function checkSignatures(
    bytes32 dataHash,
    bytes memory signatures
) public view {
    // 读取多签执行门槛
    uint256 _threshold = threshold;
    require(_threshold > 0, "WTF5005");

    // 检查签名长度足够长
    require(signatures.length >= _threshold * 65, "WTF5006");

    // 通过一个循环,检查收集的签名是否有效
    // 大概思路:
    // 1. 用ecdsa先验证签名是否有效
    // 2. 利用 currentOwner > lastOwner 确定签名来自不同多签(多签地址递增)
    // 3. 利用 isOwner[currentOwner] 确定签名者为多签持有人
    address lastOwner = address(0); 
    address currentOwner;
    uint8 v;
    bytes32 r;
    bytes32 s;
    uint256 i;
    for (i = 0; i < _threshold; i++) {
        (v, r, s) = signatureSplit(signatures, i);
        // 利用ecrecover检查签名是否有效
        currentOwner = ecrecover(keccak256(abi.encodePacked("\x19Ethereum Signed Message:\n32", dataHash)), v, r, s);
        require(currentOwner > lastOwner && isOwner[currentOwner], "WTF5007");
        lastOwner = currentOwner;

5、signatureSplit():将单个签名从打包的签名分离出来,参数分别为打包签名 signatures 和要读取的签名位置 pos。利用了内联汇编,将签名的 r,s,和 v 三个值分离出来。

/// 将单个签名从打包的签名分离出来
/// @param signatures 打包签名
/// @param pos 要读取的多签index.
function signatureSplit(bytes memory signatures, uint256 pos)
    internal
    pure
    returns (
        uint8 v,
        bytes32 r,
        bytes32 s
    )
{
    // 签名的格式:{bytes32 r}{bytes32 s}{uint8 v}
    assembly {
        let signaturePos := mul(0x41, pos)
        r := mload(add(signatures, add(signaturePos, 0x20)))
        s := mload(add(signatures, add(signaturePos, 0x40)))
        v := and(mload(add(signatures, add(signaturePos, 0x41))), 0xff)

6、encodeTransactionData():将交易数据打包并计算哈希,利用了 abi.encode()和 keccak256()函数。这个函数可以计算出一个交易的哈希,然后在链下让多签人签名并收集,再调用 execTransaction()函数执行。

/// @dev 编码交易数据
/// @param to 目标合约地址
/// @param value msg.value,支付的以太坊
/// @param data calldata
/// @param _nonce 交易的nonce.
/// @param chainid 链id
/// @return 交易哈希bytes.
function encodeTransactionData(
    address to,
    uint256 value,
    bytes memory data,
    uint256 _nonce,
    uint256 chainid
) public pure returns (bytes32) {
    bytes32 safeTxHash =
        keccak256(
            abi.encode(
                to,
                value,
                keccak256(data),
                _nonce,
                chainid
            )
        );
    return safeTxHash;

综上所述,上述代码展示了一个简单但功能完整的多签钱包合约的编写过程。虽然这种合约的应用现在更多的是在 DAO 组织中,但其实多签钱包的应用范围非常广泛,可以方便地管理各种资产,提高安全性。在以太坊生态系统中,Gnosis Safe 多签钱包是最受欢迎的多签钱包之一,管理着近 400 亿美元的资产,经过了审计和实战测试,并支持多条链(如以太坊、BSC, ,Polygon等)。如果您对多签钱包感兴趣,可以尝试使用一下,探索更多可能性。

定制开发:DexDao

© 版权声明

Related posts

No comments

No comments...