{"author":{"address":null,"user":"https://learnblockchain.cn/people/18158"},"content":{"body":"# 前言\r\n\u003e 借用维塔利克·布特林的观点：大家对硬件钱包高估了，相对于硬件钱包，多签钱包更加安全.本文快速实现一个简洁版的多签钱包合约。\r\n# 多签钱包\r\n**定义**:一种需要多个私钥签名才能完成交易的加密钱包，需要多个授权方共同签名才能执行交易。这种设计大大提高了钱包的安全性，降低了单点故障和私钥被盗的风险；\r\n#### 工作原理\r\n1.  **设置多签钱包**：创建多签钱包时，用户需要指定多个参与者（例如3人）和最低签名数量（例如2个）。这种配置通常被表示为“m-of-n”，其中m是最少签名数量，n是总参与者数量。\r\n1.  **生成公私钥对**：每个参与者生成一对公私钥，并将公钥提交给多签钱包。\r\n1.  **创建多签地址**：多签钱包根据所有公钥生成一个多签地址，所有资金将存储在这个地址中。\r\n1.  **发起交易**：当需要发起交易时，交易信息将被广播给所有参与者。\r\n1.  **签名和广播**：达到最低签名数量的参与者使用各自的私钥对交易进行签名。所有必要的签名完成后，交易将被广播到区块链网络，并最终被矿工确认。\r\n#### 优点\r\n-   **提高安全性**：多签钱包的设计大大增强了资金安全性，即使某个私钥被盗，黑客也无法单独完成交易。\r\n-   **防范单点故障**：由于需要多个签名才能执行交易，多签钱包有效防止了因单个私钥丢失或损坏而导致的资金不可用。\r\n-   **增强透明度和信任**：在团队或机构中使用多签钱包，可以确保所有交易都经过多个成员的同意，增加了操作的透明度和信任度。\r\n-   **访问控制和权限管理**：多签钱包可以灵活地设置签名规则，满足不同场景的需求，如家庭理财、企业资金管理等。\r\n#### 场景\r\n1.  **资金安全**：多签钱包可以有效防止因单个私钥丢失或被盗而导致的资金损失。例如，2/3多签模式中，即使其中一个私钥丢失或被盗，只要有其他两个私钥完成签名授权，资金仍然安全。\r\n1.  **企业财务管理**：企业可以使用多签钱包来管理公司资金，确保资金的安全性和合规性。多个管理者共同控制钱包，任何一笔交易都需要经过多个管理者的同意。\r\n1.  **众筹项目**：在众筹项目中，多签钱包可以确保资金的安全性和透明度，防止项目方单方面挪用资金。\r\n1.  **去中心化自治组织（DAO）** ：DAO 可以使用多签钱包来管理社区资金，确保所有重要决策都经过社区成员的共同同意。\r\n# 合约开发\r\n```\r\n// SPDX-License-Identifier: MIT\r\n// author: @0xAA_Science from wtf.academy\r\npragma solidity ^0.8.21;\r\nimport \"hardhat/console.sol\";\r\n/// 基于签名的多签钱包\r\ncontract MultiSigWallet {\r\n    event ExecutionSuccess(bytes32 txHash);    // 交易成功事件\r\n    event ExecutionFailure(bytes32 txHash);    // 交易失败事件\r\n    address[] public owners;                   // 多签持有人数组 \r\n    mapping(address =\u003e bool) public isOwner;   // 记录一个地址是否为多签\r\n    uint256 public ownerCount;                 // 多签持有人数量\r\n    uint256 public threshold;                  // 多签执行门槛，交易至少有n个多签人签名才能被执行。\r\n    uint256 public nonce;                      // nonce，防止签名重放攻击\r\n\r\n    receive() external payable {}\r\n\r\n    // 构造函数，初始化owners, isOwner, ownerCount, threshold \r\n    constructor(        \r\n        address[] memory _owners,\r\n        uint256 _threshold\r\n    ) {\r\n        _setupOwners(_owners, _threshold);\r\n    }\r\n\r\n    /// @dev 初始化owners, isOwner, ownerCount,threshold \r\n    /// @param _owners: 多签持有人数组\r\n    /// @param _threshold: 多签执行门槛，至少有几个多签人签署了交易\r\n    function _setupOwners(address[] memory _owners, uint256 _threshold) internal {\r\n        // threshold没被初始化过\r\n        require(threshold == 0, \"WTF5000\");\r\n        // 多签执行门槛 小于或等于 多签人数\r\n        require(_threshold \u003c= _owners.length, \"WTF5001\");\r\n        // 多签执行门槛至少为1\r\n        require(_threshold \u003e= 1, \"WTF5002\");\r\n\r\n        for (uint256 i = 0; i \u003c _owners.length; i++) {\r\n            address owner = _owners[i];\r\n            // 多签人不能为0地址，本合约地址，不能重复\r\n            require(owner != address(0) \u0026\u0026 owner != address(this) \u0026\u0026 !isOwner[owner], \"WTF5003\");\r\n            owners.push(owner);\r\n            isOwner[owner] = true;\r\n        }\r\n        ownerCount = _owners.length;\r\n        threshold = _threshold;\r\n    }\r\n\r\n    /// @dev 在收集足够的多签签名后，执行交易\r\n    /// @param to 目标合约地址\r\n    /// @param value msg.value，支付的以太坊\r\n    /// @param data calldata\r\n    /// @param signatures 打包的签名，对应的多签地址由小到达，方便检查。 ({bytes32 r}{bytes32 s}{uint8 v}) (第一个多签的签名, 第二个多签的签名 ... )\r\n    function execTransaction(\r\n        address to,\r\n        uint256 value,\r\n        bytes memory data,\r\n        bytes memory signatures\r\n    ) public payable virtual returns (bool success) {\r\n        // 编码交易数据，计算哈希\r\n        bytes32 txHash = encodeTransactionData(to, value, data, nonce, block.chainid);\r\n        nonce++;  // 增加nonce\r\n        checkSignatures(txHash, signatures); // 检查签名\r\n        // 利用call执行交易，并获取交易结果\r\n        (success, ) = to.call{value: value}(data);\r\n        //require(success , \"WTF5004\");\r\n        if (success) emit ExecutionSuccess(txHash);\r\n        else emit ExecutionFailure(txHash);\r\n    }\r\n\r\n    /**\r\n     * @dev 检查签名和交易数据是否对应。如果是无效签名，交易会revert\r\n     * @param dataHash 交易数据哈希\r\n     * @param signatures 几个多签签名打包在一起\r\n     */\r\n    function checkSignatures(\r\n        bytes32 dataHash,\r\n        bytes memory signatures\r\n    ) public view {\r\n        // 读取多签执行门槛\r\n        uint256 _threshold = threshold;\r\n        require(_threshold \u003e 0, \"WTF5005\");\r\n\r\n        // 检查签名长度足够长\r\n        require(signatures.length \u003e= _threshold * 65, \"WTF5006\");\r\n\r\n        // 通过一个循环，检查收集的签名是否有效\r\n        // 大概思路：\r\n        // 1. 用ecdsa先验证签名是否有效\r\n        // 2. 利用 currentOwner \u003e lastOwner 确定签名来自不同多签（多签地址递增）\r\n        // 3. 利用 isOwner[currentOwner] 确定签名者为多签持有人\r\n        address lastOwner = address(0); \r\n        address currentOwner;\r\n        uint8 v;\r\n        bytes32 r;\r\n        bytes32 s;\r\n        uint256 i;\r\n        for (i = 0; i \u003c _threshold; i++) {\r\n            (v, r, s) = signatureSplit(signatures, i);\r\n            // 利用ecrecover检查签名是否有效\r\n            currentOwner = ecrecover(keccak256(abi.encodePacked(\"\\x19Ethereum Signed Message:\\n32\", dataHash)), v, r, s);\r\n            console.log(currentOwner \u003e lastOwner \u0026\u0026 isOwner[currentOwner]);\r\n            console.log(currentOwner);\r\n            console.log(lastOwner);\r\n            console.log(currentOwner \u003e lastOwner);\r\n            console.log(isOwner[currentOwner]);\r\n            console.log(\"----\",currentOwner);\r\n            require(currentOwner \u003e lastOwner \u0026\u0026 isOwner[currentOwner], \"WTF5007\");\r\n            lastOwner = currentOwner;\r\n            console.log(lastOwner);\r\n        }\r\n    }\r\n    \r\n    /// 将单个签名从打包的签名分离出来\r\n    /// @param signatures 打包的多签\r\n    /// @param pos 要读取的多签index.\r\n    function signatureSplit(bytes memory signatures, uint256 pos)\r\n        internal\r\n        pure\r\n        returns (\r\n            uint8 v,\r\n            bytes32 r,\r\n            bytes32 s\r\n        )\r\n    {\r\n        // 签名的格式：{bytes32 r}{bytes32 s}{uint8 v}\r\n        assembly {\r\n            let signaturePos := mul(0x41, pos)\r\n            r := mload(add(signatures, add(signaturePos, 0x20)))\r\n            s := mload(add(signatures, add(signaturePos, 0x40)))\r\n            v := and(mload(add(signatures, add(signaturePos, 0x41))), 0xff)\r\n        }\r\n    }\r\n\r\n    /// @dev 编码交易数据\r\n    /// @param to 目标合约地址\r\n    /// @param value msg.value，支付的以太坊\r\n    /// @param data calldata\r\n    /// @param _nonce 交易的nonce.\r\n    /// @param chainid 链id\r\n    /// @return 交易哈希bytes.\r\n    function encodeTransactionData(\r\n        address to,\r\n        uint256 value,\r\n        bytes memory data,\r\n        uint256 _nonce,\r\n        uint256 chainid\r\n    ) public pure returns (bytes32) {\r\n        bytes32 safeTxHash =\r\n            keccak256(\r\n                abi.encode(\r\n                    to,\r\n                    value,\r\n                    keccak256(data),\r\n                    _nonce,\r\n                    chainid\r\n                )\r\n            );\r\n        return safeTxHash;\r\n    }\r\n}\r\n# 编译指令\r\n# npx hardhat compile\r\n```\r\n# 合约测试\r\n**说明**：1.先启动一下本地的网络节点，npx hardhat node，2.先给多签钱包合约转入一定量的代币，3.进行多签测试\r\n```\r\nconst {ethers,getNamedAccounts,deployments} = require(\"hardhat\");\r\nconst { assert,expect } = require(\"chai\");\r\ndescribe(\"MultisigWallet\",function(){\r\n    let MultisigWallet;//合约\r\n    let firstAccount//第一个账户\r\n    let secondAccount//第二个账户\r\n    let owner, signer1, signer2, signer3;\r\n    const amount = ethers.utils.parseEther('10');\r\n    const provider = new ethers.providers.JsonRpcProvider('http://127.0.0.1:8545');\r\n    beforeEach(async function(){\r\n        await deployments.fixture([\"MultiSigWallet\"]);\r\n        [owner, signer1, signer2, signer3]=await ethers.getSigners();\r\n        firstAccount=(await getNamedAccounts()).firstAccount;\r\n        secondAccount=(await getNamedAccounts()).secondAccount;\r\n        const MultisigWalletDeployment = await deployments.get(\"MultiSigWallet\");\r\n        MultisigWallet = await ethers.getContractAt(\"MultiSigWallet\",MultisigWalletDeployment.address);//已经部署的合约交互\r\n    });\r\n    \r\n    describe(\"多签钱包\", function () {\r\n        it(\"测试\", async function () {\r\n            //向多签钱包合约转100eth\r\n            console.log(amount)\r\n            // 创建一个交易对象  转账 1 ETH 到多签钱包\r\n            // const tx =owner.sendTransaction( {\r\n            // to: signer1.address,\r\n            // value: amount,\r\n            // // gasLimit: ethers.BigNumber.from('50000'),\r\n            // // gasPrice: ethers.utils.parseUnits('100', 'gwei')\r\n            // });\r\n            // await tx.wait();\r\n            // console.log(await provider.getBalance(signer1.address))\r\n            // console.log(\"Transaction 10 ETH MultisigWallet合约:\", MultisigWallet.address);\r\n            //\r\n            //私有转账\r\n            const privateKey = \"0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80\";\r\n            // 创建 Wallet 实例\r\n            const wallet = new ethers.Wallet(privateKey, provider);\r\n            const balance = await wallet.getBalance();\r\n            console.log(\"当前余额:\", ethers.utils.formatEther(balance), \"ETH\");\r\n            const tx1 ={\r\n                to: signer1.address,\r\n                value: amount,\r\n            }\r\n            // 发送交易\r\n            const txResponse = await wallet.sendTransaction(tx1);\r\n            console.log(\"交易发送中...\", txResponse.hash);\r\n\r\n            // 等待交易确认\r\n            const txReceipt = await txResponse.wait();\r\n            console.log(\"交易确认:\", txReceipt.transactionHash);\r\n            console.log(\"查看signer1余额\",`${ethers.utils.formatEther(await provider.getBalance(signer1.address))} ETH`)\r\n            // 获取交易\r\n        const hash= await MultisigWallet.encodeTransactionData(owner.address,amount,\"0x\",0,31337);\r\n        console.log(\"Hash\",hash)\r\n           // 提交交易\r\n           //owner3交易签名\r\n           const signature1 = await signer2.signMessage(ethers.utils.arrayify(hash));\r\n           console.log(\"signer2 Signature:\", signature1);\r\n           //owner2交易签名\r\n           const signature2 = await signer1.signMessage(ethers.utils.arrayify(hash))\r\n           console.log(\"signer1 Signature:\", signature2)\r\n           //打包签名\r\n           let Signatures=signature1+signature2.slice(2);\r\n           console.log(\"signer1signer2Signatures\",Signatures)\r\n           await MultisigWallet.execTransaction(owner.address,amount,\"0x\",Signatures)\r\n           console.log(\"转账成功\")\r\n        });\r\n      });\r\n    \r\n})\r\n# 测试指令\r\n# npx hardhat test ./test/xxx.js\r\n```\r\n# 合约部署\r\n**说明**：3个多签地址，交易执行门槛设为2\r\n```\r\nmodule.exports = async function({getNamedAccounts,deployments}) {\r\n    const  firstAccount= (await getNamedAccounts()).firstAccount;\r\n    const  secondAccount= (await getNamedAccounts()).secondAccount;\r\n    const [addr1,addr2,addr3]=await ethers.getSigners();\r\n    const {deploy,log}=deployments;\r\n    let MultiSigArray=[addr1.address,addr2.address,addr3.address];\r\n    let MultiSigValue=2;\r\n    const MultiSigWallet=await deploy(\"MultiSigWallet\",{\r\n        from:firstAccount,\r\n        args: [MultiSigArray,MultiSigValue],//参数 \r\n        log: true,\r\n    })\r\n    console.log('MultiSigWallet合约地址',MultiSigWallet.address)\r\n}\r\nmodule.exports.tags = [\"all\", \"MultiSigWallet\"];\r\n# 部署指令\r\n# npx hardhat compile\r\n```\r\n# 总结\r\n以上就是多签钱包合约开发、测试、部署全流程以及多签钱包的相关介绍，`注意`：测试时，打包签名的账号从后向前拼接；","title":"快速实现一个极简版多签钱包"},"history":null,"timestamp":1740980359,"version":1}