快速实现一个极简版多签钱包

155 阅读8分钟

前言

借用维塔利克·布特林的观点:大家对硬件钱包高估了,相对于硬件钱包,多签钱包更加安全.本文快速实现一个简洁版的多签钱包合约。

多签钱包

定义:一种需要多个私钥签名才能完成交易的加密钱包,需要多个授权方共同签名才能执行交易。这种设计大大提高了钱包的安全性,降低了单点故障和私钥被盗的风险;

工作原理
  1. 设置多签钱包:创建多签钱包时,用户需要指定多个参与者(例如3人)和最低签名数量(例如2个)。这种配置通常被表示为“m-of-n”,其中m是最少签名数量,n是总参与者数量。
  2. 生成公私钥对:每个参与者生成一对公私钥,并将公钥提交给多签钱包。
  3. 创建多签地址:多签钱包根据所有公钥生成一个多签地址,所有资金将存储在这个地址中。
  4. 发起交易:当需要发起交易时,交易信息将被广播给所有参与者。
  5. 签名和广播:达到最低签名数量的参与者使用各自的私钥对交易进行签名。所有必要的签名完成后,交易将被广播到区块链网络,并最终被矿工确认。
优点
  • 提高安全性:多签钱包的设计大大增强了资金安全性,即使某个私钥被盗,黑客也无法单独完成交易。
  • 防范单点故障:由于需要多个签名才能执行交易,多签钱包有效防止了因单个私钥丢失或损坏而导致的资金不可用。
  • 增强透明度和信任:在团队或机构中使用多签钱包,可以确保所有交易都经过多个成员的同意,增加了操作的透明度和信任度。
  • 访问控制和权限管理:多签钱包可以灵活地设置签名规则,满足不同场景的需求,如家庭理财、企业资金管理等。
场景
  1. 资金安全:多签钱包可以有效防止因单个私钥丢失或被盗而导致的资金损失。例如,2/3多签模式中,即使其中一个私钥丢失或被盗,只要有其他两个私钥完成签名授权,资金仍然安全。
  2. 企业财务管理:企业可以使用多签钱包来管理公司资金,确保资金的安全性和合规性。多个管理者共同控制钱包,任何一笔交易都需要经过多个管理者的同意。
  3. 众筹项目:在众筹项目中,多签钱包可以确保资金的安全性和透明度,防止项目方单方面挪用资金。
  4. 去中心化自治组织(DAO) :DAO 可以使用多签钱包来管理社区资金,确保所有重要决策都经过社区成员的共同同意。

合约开发

// SPDX-License-Identifier: MIT
// author: @0xAA_Science from wtf.academy
pragma solidity ^0.8.21;
import "hardhat/console.sol";
/// 基于签名的多签钱包
contract MultiSigWallet {
    event ExecutionSuccess(bytes32 txHash);    // 交易成功事件
    event ExecutionFailure(bytes32 txHash);    // 交易失败事件
    address[] public owners;                   // 多签持有人数组 
    mapping(address => bool) public isOwner;   // 记录一个地址是否为多签
    uint256 public ownerCount;                 // 多签持有人数量
    uint256 public threshold;                  // 多签执行门槛,交易至少有n个多签人签名才能被执行。
    uint256 public nonce;                      // nonce,防止签名重放攻击

    receive() external payable {}

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

    /// @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;
    }

    /// @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);
    }

    /**
     * @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);
            console.log(currentOwner > lastOwner && isOwner[currentOwner]);
            console.log(currentOwner);
            console.log(lastOwner);
            console.log(currentOwner > lastOwner);
            console.log(isOwner[currentOwner]);
            console.log("----",currentOwner);
            require(currentOwner > lastOwner && isOwner[currentOwner], "WTF5007");
            lastOwner = currentOwner;
            console.log(lastOwner);
        }
    }
    
    /// 将单个签名从打包的签名分离出来
    /// @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)
        }
    }

    /// @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;
    }
}
# 编译指令
# npx hardhat compile

合约测试

说明:1.先启动一下本地的网络节点,npx hardhat node,2.先给多签钱包合约转入一定量的代币,3.进行多签测试

const {ethers,getNamedAccounts,deployments} = require("hardhat");
const { assert,expect } = require("chai");
describe("MultisigWallet",function(){
    let MultisigWallet;//合约
    let firstAccount//第一个账户
    let secondAccount//第二个账户
    let owner, signer1, signer2, signer3;
    const amount = ethers.utils.parseEther('10');
    const provider = new ethers.providers.JsonRpcProvider('http://127.0.0.1:8545');
    beforeEach(async function(){
        await deployments.fixture(["MultiSigWallet"]);
        [owner, signer1, signer2, signer3]=await ethers.getSigners();
        firstAccount=(await getNamedAccounts()).firstAccount;
        secondAccount=(await getNamedAccounts()).secondAccount;
        const MultisigWalletDeployment = await deployments.get("MultiSigWallet");
        MultisigWallet = await ethers.getContractAt("MultiSigWallet",MultisigWalletDeployment.address);//已经部署的合约交互
    });
    
    describe("多签钱包", function () {
        it("测试", async function () {
            //向多签钱包合约转100eth
            console.log(amount)
            // 创建一个交易对象  转账 1 ETH 到多签钱包
            // const tx =owner.sendTransaction( {
            // to: signer1.address,
            // value: amount,
            // // gasLimit: ethers.BigNumber.from('50000'),
            // // gasPrice: ethers.utils.parseUnits('100', 'gwei')
            // });
            // await tx.wait();
            // console.log(await provider.getBalance(signer1.address))
            // console.log("Transaction 10 ETH MultisigWallet合约:", MultisigWallet.address);
            //
            //私有转账
            const privateKey = "0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80";
            // 创建 Wallet 实例
            const wallet = new ethers.Wallet(privateKey, provider);
            const balance = await wallet.getBalance();
            console.log("当前余额:", ethers.utils.formatEther(balance), "ETH");
            const tx1 ={
                to: signer1.address,
                value: amount,
            }
            // 发送交易
            const txResponse = await wallet.sendTransaction(tx1);
            console.log("交易发送中...", txResponse.hash);

            // 等待交易确认
            const txReceipt = await txResponse.wait();
            console.log("交易确认:", txReceipt.transactionHash);
            console.log("查看signer1余额",`${ethers.utils.formatEther(await provider.getBalance(signer1.address))} ETH`)
            // 获取交易
        const hash= await MultisigWallet.encodeTransactionData(owner.address,amount,"0x",0,31337);
        console.log("Hash",hash)
           // 提交交易
           //owner3交易签名
           const signature1 = await signer2.signMessage(ethers.utils.arrayify(hash));
           console.log("signer2 Signature:", signature1);
           //owner2交易签名
           const signature2 = await signer1.signMessage(ethers.utils.arrayify(hash))
           console.log("signer1 Signature:", signature2)
           //打包签名
           let Signatures=signature1+signature2.slice(2);
           console.log("signer1signer2Signatures",Signatures)
           await MultisigWallet.execTransaction(owner.address,amount,"0x",Signatures)
           console.log("转账成功")
        });
      });
    
})
# 测试指令
# npx hardhat test ./test/xxx.js

合约部署

说明:3个多签地址,交易执行门槛设为2

module.exports = async function({getNamedAccounts,deployments}) {
    const  firstAccount= (await getNamedAccounts()).firstAccount;
    const  secondAccount= (await getNamedAccounts()).secondAccount;
    const [addr1,addr2,addr3]=await ethers.getSigners();
    const {deploy,log}=deployments;
    let MultiSigArray=[addr1.address,addr2.address,addr3.address];
    let MultiSigValue=2;
    const MultiSigWallet=await deploy("MultiSigWallet",{
        from:firstAccount,
        args: [MultiSigArray,MultiSigValue],//参数 
        log: true,
    })
    console.log('MultiSigWallet合约地址',MultiSigWallet.address)
}
module.exports.tags = ["all", "MultiSigWallet"];
# 部署指令
# npx hardhat compile

总结

以上就是多签钱包合约开发、测试、部署全流程以及多签钱包的相关介绍,注意:测试时,打包签名的账号从后向前拼接;