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

  • 木西
  • 发布于 13小时前
  • 阅读 58

前言借用维塔利克·布特林的观点:大家对硬件钱包高估了,相对于硬件钱包,多签钱包更加安全.本文快速实现一个简洁版的多签钱包合约。多签钱包定义:一种需要多个私钥签名才能完成交易的加密钱包,需要多个授权方共同签名才能执行交易。这种设计大大提高了钱包的安全性,降低了单点故障和私钥被盗的风险;工作

前言

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

多签钱包

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

工作原理

  1. 设置多签钱包:创建多签钱包时,用户需要指定多个参与者(例如3人)和最低签名数量(例如2个)。这种配置通常被表示为“m-of-n”,其中m是最少签名数量,n是总参与者数量。
  2. 生成公私钥对:每个参与者生成一对公私钥,并将公钥提交给多签钱包。
  3. 创建多签地址:多签钱包根据所有公钥生成一个多签地址,所有资金将存储在这个地址中。
  4. 发起交易:当需要发起交易时,交易信息将被广播给所有参与者。
  5. 签名和广播:达到最低签名数量的参与者使用各自的私钥对交易进行签名。所有必要的签名完成后,交易将被广播到区块链网络,并最终被矿工确认。

    优点

    • 提高安全性:多签钱包的设计大大增强了资金安全性,即使某个私钥被盗,黑客也无法单独完成交易。
    • 防范单点故障:由于需要多个签名才能执行交易,多签钱包有效防止了因单个私钥丢失或损坏而导致的资金不可用。
    • 增强透明度和信任:在团队或机构中使用多签钱包,可以确保所有交易都经过多个成员的同意,增加了操作的透明度和信任度。
    • 访问控制和权限管理:多签钱包可以灵活地设置签名规则,满足不同场景的需求,如家庭理财、企业资金管理等。

      场景

  6. 资金安全:多签钱包可以有效防止因单个私钥丢失或被盗而导致的资金损失。例如,2/3多签模式中,即使其中一个私钥丢失或被盗,只要有其他两个私钥完成签名授权,资金仍然安全。
  7. 企业财务管理:企业可以使用多签钱包来管理公司资金,确保资金的安全性和合规性。多个管理者共同控制钱包,任何一笔交易都需要经过多个管理者的同意。
  8. 众筹项目:在众筹项目中,多签钱包可以确保资金的安全性和透明度,防止项目方单方面挪用资金。
  9. 去中心化自治组织(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


# 总结
以上就是多签钱包合约开发、测试、部署全流程以及多签钱包的相关介绍,`注意`:测试时,打包签名的账号从后向前拼接;
点赞 0
收藏 0
分享
本文参与登链社区写作激励计划 ,好文好收益,欢迎正在阅读的你也加入。

0 条评论

请先 登录 后评论
木西
木西
江湖只有他的大名,没有他的介绍。