当前位置: 首页 > news >正文

solidity学习之多签钱包

什么是多签钱包

多签钱包是一种特殊的钱包,可以添加多个签名用户,在执行交易的时候需要多个持有者同时签名才能提交,比如3个用户的多签钱包需要2个以上的用户同时签名。

这种设计可以有效防止单点故障,保证资产的安全,在dao群中有广泛的应用。

实现逻辑

多签钱包其实是一个智能合约,在合约中存储了多签持有者的信息。

执行交易时,需要先将交易组装成data,计算出hash。拿到交易hash后,多签用户需要分别对hash进行签名,并且将得到的签名拼接成最终的签名作为参数。

将交易data和signature作为参数调用合约中的方法,合约做的工作是:

  1. 判断签名数量需要大于多签规定的签名数量
  2. 拆分签名
  3. 对应每条签名通过ecrecover还原出签名地址,判断该地址是否存储于合约中
  4. 满足签名条件后,根据data执行交易

具体实现

ecrecover

ecrecover是solidity内置的验签方法,定义如下:

function ecrecover(bytes32 hash, uint8 v, bytes32 r, bytes32 s) public pure returns (address)

其中hash为交易哈希,也就是签名的msg,v,r,s是从签名中拆分得到的,返回值为签名的公钥。

签名拆分

这里使用的是ECDSA标准的签名,

一个标准的 ECDSA 签名是:

  • bytes32 r
  • bytes32 s
  • uint8 v(27 或 28,也可能是 0 或 1)

总共65字节的内容,因此要写一个对signature进行拆分的方法,得到v、r、s

function signatureSplit(bytes memory signatures, uint256 pos)internalpurereturns (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)}
}

这里使用了内联汇编的写法,因为涉及到了内存的读取。

signatures是拼接后的签名,pos代表的是当前读取的签名在signatures中是第几个,即索引值。

let signaturePos := mul(0x41, pos)是用来定位当前拆分出签名的偏移量,因为每个签名的长度是65字节,换算成16进制就是0x41,所以偏移量就是0x41*pos

r := mload(add(signatures, add(signaturePos, 0x20)))中,mload的作用是从某个内存地址开始,向后读取固定32字节的内容,而add(signatures, add(signaturePos, 0x20))从内部到外部的两个add分别表示:

  • add(signaturePos, 0x20)表示signaturePos0x20偏移量相加,因为bytes类型的前32字节是头部,而非实际数据,所以读取时先偏移到signaturePos,即签名起点,再向后偏移32个字节。
  • add(signatures, pos)指的是从拼接签名的起始位置,偏移到pos的位置

这两个add,一个是偏移量的相加,一个是位置的移动,但在内联汇编计算中都是使用add方法,因为signatures是一个指针,指向的地址也是用偏移字节数表示的,代表从内存0的位置偏移的数量。

用同样的原理可以取出s和v,要注意的是v的字节数为1,而mload固定取32字节,所以使用and()方法与0xff做了一个与操作,得到最低位1字节的值。

设计好验签逻辑之后,就可以开始写合约内容了。

	address[] public owners;                   // 多签持有人数组 mapping(address => bool) public isOwner;   // 记录一个地址是否为多签持有人uint256 public ownerCount;                 // 多签持有人数量uint256 public threshold;                  // 多签执行门槛,交易至少有n个多签人签名才能被执行。uint256 public nonce;                      // nonce,防止签名重放攻击constructor(        address[] memory _owners,uint256 _threshold
) {_setupOwners(_owners, _threshold);
}/// @param _owners: 多签持有人数组
/// @param _threshold: 多签执行门槛,至少有几个多签人签署了交易
function _setupOwners(address[] memory _owners, uint256 _threshold) internal {// 多签执行门槛 小于或等于 多签人数require(_threshold <= _owners.length, "invalid threshold");// 多签执行门槛至少为1require(_threshold >= 1, "threshold at least 1");for (uint256 i = 0; i < _owners.length; i++) {address owner = _owners[i];// 多签人不能为0地址,本合约地址,不能重复require(owner != address(0) && owner != address(this) && !isOwner[owner], "owner can not be repeated");owners.push(owner);isOwner[owner] = true;}ownerCount = _owners.length;threshold = _threshold;
}

此处是简化多签钱包的逻辑,不做多签持有人的变更,持有人的列表和threshold都是固定不变的。

然后写一个打包交易hash的方法,在参数中增加了一个chainid,这是为了防止拿到签名之后可以去其他链执行,进行交易重放。

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

然后写一个验签方法:

/*** @dev 检查签名和交易数据是否对应。如果是无效签名,交易会revert* @param dataHash 交易数据哈希* @param signatures 几个多签签名打包在一起*/
function checkSignatures(bytes32 dataHash,bytes memory signatures
) public view {// 读取多签执行门槛uint256 _threshold = threshold;require(_threshold > 0, "threshold not set");// 检查签名长度足够长require(signatures.length >= _threshold * 65, "signature not satisify threshold");// 通过一个循环,检查收集的签名是否有效// 大概思路:// 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], "singer not owner");lastOwner = currentOwner;}
}

几个要注意的地方:

  1. threshold是否满足的判断是由signatures的长度来判断的,因为单个签名的长度固定是65字节。
  2. ecrecover的时候拼接了\x19Ethereum Signed Message:\n32,这是因为在使用eth_sign或者钱包签名的时候,会自动在前面加上这一段话,用于标识是签名而非真实的交易数据,所以验签的时候也需要加上。
  3. lastOwner的记录是因为signatures的拼接是根据address从小到大进行拼的,这样保证了多签拼接顺序的固定,所以验签时还需要判断address之间的大小关系。

最后实现一个执行合约的方法:

/// @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++;  // 增加noncecheckSignatures(txHash, signatures); // 检查签名// 利用call执行交易,并获取交易结果(success, ) = to.call{value: value}(data);if (success) emit ExecutionSuccess(txHash);else emit ExecutionFailure(txHash);
}
http://www.sczhlp.com/news/8380/

相关文章:

  • 软考系统分析师每日学习卡 | [日期:2025-08-08] | [今日主题:数据库三级模式两级映射]
  • 双指针
  • QML给Rectangle添加阴影
  • Python函数实战之ATM与购物车系统
  • 【鲜花】浙江游记
  • C++中 . 与- 的使用场景
  • 七天零基础学java(第二天)--赵姗姗
  • 业财融合:从思维差异到组织重构的转型指南 - 智慧园区
  • 2025-8-8 重新开始启动 - 小
  • 对于构造函数的笔记
  • Mac 在Dify平台中接入Ollama本地部署的模型
  • 求区间 [L,R] 中素数/最小公倍数的个数
  • 8.8随笔
  • m3u8 demo
  • G. Shorten the Array题解
  • 多项式基础函数
  • MX-2025 盖世计划 C 班 Day 5 复盘
  • 我是如何操纵Bugcrowd平台排名的 - 漏洞挖掘技术解析
  • 标注的原理:少而完备,监督模型训练的根本
  • channel
  • ARP协议详解:网络通信的幕后英雄
  • 20250808 做题记录
  • GPT-5 全面升级!ModelGate 平台首发上线体验
  • 4.1 ~ 4.2 EXTI外部中断 - LI,Yi
  • 实用指南:Nginx 配置负载均衡(详细版)
  • jarvisoj_fm 1
  • 2025.8.8打卡
  • 【项目落地】企业最高性价比AI项目:私有RAG知识库的跨行业赋能实践
  • 练习cf2025A. Two Screens
  • 8/8