写点什么

150 行代码创建一个多签钱包,智能合约实战项目

作者:加密先生
  • 2023-03-09
    广东
  • 本文字数:4307 字

    阅读完需:约 14 分钟

150行代码创建一个多签钱包,智能合约实战项目

什么是多签钱包?就是需要多个人同时签名才能操作的钱包。大家用过钱包都知道,在你进行买卖或者转账操作的时候,正常的钱包只需要一次签名就可以了,但是多签钱包需要多个人授权签名,才能进行操作,这样安全性就大大增加。


更为重要的是,如果是普通钱包,可能私钥丢失了就没办法了。但是对于多签钱包来说,如果其中一人失了私钥,其他人仍然可以访问钱包和资金。所以 V 神曾说过,多签钱包要比硬件钱包更加安全。


不过很多人不太了解的是,多签钱包的原理是什么?如何开发一个多签钱包合约呢?今天就给大家解答一下这个问题,同时把相关的合约代码也分享出来。

一、创建多签钱包合约


1、设置多签人和门槛(链上):部署多签合约时,我们需要初始化多签人列表和执行门槛(至少 n 个多签人签名授权后,交易才能执行)。Gnosis Safe 多签钱包支持增加/删除多签人以及改变执行门槛,但在咱们的极简版中不考虑这一功能。


2、创建交易(链下):一笔待授权的交易包含以下内容


  • to:目标合约。

  • value:交易发送的以太坊数量。

  • data:calldata,包含调用函数的选择器和参数。

  • nonce:初始为 0,随着多签合约每笔成功执行的交易递增的值,可以防止签名重放攻击。

  • chainid:链 id,防止不同链的签名重放攻击。


3、收集多签签名(链下):将上一步的交易 ABI 编码并计算哈希,得到交易哈希,然后让多签人签名,并拼接到一起的到打包签名。对 ABI 编码和哈希不了解的,可以看 WTF Solidity 极简教程第 27 讲和第 28 讲。


交易哈希: 0xc1b055cf8e78338db21407b425114a2e258b0318879327945b661bfdea570e66


多签人 A 签名: 0xd6a56c718fc16f283512f90e16f2e62f888780a712d15e884e300c51e5b100de2f014ad71bcb6d97946ef0d31346b3b71eb688831abedaf41b33486b416129031c


多签人 B 签名: 0x2184f70a17f14426865bda8ebe391508b8e3984d16ce6d90905ae8beae7d75fd435a7e51d837881d820414ebaf0ff16074204c75b33d66928edcf8dd398249861b


打包签名:0xd6a56c718fc16f283512f90e16f2e62f888780a712d15e884e300c51e5b100de2f014ad71bcb6d97946ef0d31346b3b71eb688831abedaf41b33486b416129031c2184f70a17f14426865bda8ebe391508b8e3984d16ce6d90905ae8beae7d75fd435a7e51d837881d820414ebaf0ff16074204c75b33d66928edcf8dd398249861b


4、调用多签合约的执行函数,验证签名并执行交易(链上)。


二、事件分析


MultisigWallet 合约有 2 个事件,ExecutionSuccess 和 ExecutionFailure,分别在交易成功和失败时释放,参数为交易哈希。


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


三、状态变量


MultisigWallet 合约有 5 个状态变量:


  • owners:多签持有人数组

  • isOwner:address => bool 的映射,记录一个地址是否为多签持有人。

  • ownerCount:多签持有人数量

  • threshold:多签执行门槛,交易至少有 n 个多签人签名才能被执行。

  • nonce:初始为 0,随着多签合约每笔成功执行的交易递增的值,可以防止签名重放攻击。


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


四、函数编写


MultisigWallet 合约有 6 个函数:


1、构造函数:调用_setupOwners(),初始化和多签持有人和执行门槛相关的变量。


构造函数,初始化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,发送的以太坊数额 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;
}


综上,就是此次极简版多签钱包合约的编写过程,仅用了不到 150 行代码,就完成了。其实多签钱包现在更多的应用在 DAO 组织里面,方便管理社区的基金。而 Gnosis Safe 多签钱包是以太坊最流行的多签钱包,管理近 400 亿美元资产,合约经过审计和实战测试,支持多链(以太坊,BSC,Polygon 等)。大家如果有兴趣,也可以去使用一下。


大家如果有兴趣,也可以找我开发多签钱包,wx:btc6540,tg:@btc6540

用户头像

加密先生

关注

区块链dapp开发,v:btc6540 2023-02-07 加入

区块链工程师,专业开发

评论

发布
暂无评论
150行代码创建一个多签钱包,智能合约实战项目_智能合约_加密先生_InfoQ写作社区