Ownbit 和 Gnosis 代表了当前主流的两种ETH多签实现方式
近期 Ownbit 多签增长迅速,单单 ETH/ERC20 多签一项,管理的资金总额已经超过了1亿美金。Gnosis 是另一个使用较为广泛的 ETH/ERC20 多签钱包。
Ownbit 和 Gnosis 均通过合约账户实现以太坊多签,但是其实现的逻辑却迥然不同。分别代表了当前两种主流的实现方式,我们通过合约源码来讲解实现原理和各自的优缺点。
首先,我们在 etherscan 上分别选取一个 Ownbit 和 Gnosis 多签地址,并在 “Contract” 页面上查看相应的源码。Ownbit 多签合约名称为:OwnbitMultiSig,而 Gnosis 多签合约名称为:MultiSigWallet。
Ownbit 多签地址例一:https://cn.etherscan.com/address/0xd1b33369848b005330df37f80554cf441114f39b#code
Gnosis 多签地址例一:https://etherscan.io/address/0xcafe1a77e84698c83ca8931f54a755176ef75f2c#code
注:etherscan 上的合约源码是可以自由上传的。关于如何上传合约源码到 etherscan 可以参考下面的网址:
https://ownbit.io/h5/app/prompt/pulish_ms_source_code_to_etherscan_zh.html
一个 M-N 多签的含义,以 3-5 多签为例,是指 5 个人管理资产,3 个人同意的情况下,可以花费该笔资产。在以太坊中,一个地址(私钥)代表一个人。如何表示你同意花费某笔资产?有两种方式:
用你的私钥对相应的花费(金额、目标地址等等)进行签名,并给出签名结果;
用你的私钥发送一笔以太坊交易,去调用某个特定接口,并给予特定参数;
Ownbit 多签使用了第一种方法,而 Gnosis 多签使用了第二种方法。
Ownbit 多签和 Gnosis 多签在构造函数上几乎一致,只是在一些细节处理上 Ownbit 做了一些优化。
constructor(address[] _owners, uint _required) public validRequirement(_owners.length, _required) {
for (uint i = 0; i < _owners.length; i++) {
//onwer should be distinct, and non-zero
if (isOwner[_owners[i]] || _owners[i] == address(0x0)) {
revert(); // Gnosis 此处为 throw
}
isOwner[_owners[i]] = true;
}
owners = _owners;
required = _required;
}
构造函数验证传入的 onwer 地址的唯一性和非零,以及 owner 人数和最少签名人数的常规检查。
throw 作为关键字和 revert 功能一致,只是 revert 会退还剩余的气,而 throw 会消耗掉剩余的气(气的花费上,throw 类似于 assert)。并且,throw 已经不推荐使用,而且在未来的版本中将被彻底去除。因此,新开发的合约,应使用 revert 替换 throw。
Gnosis 实现多签逻辑的过程如下:
function submitTransaction(address destination, uint value, bytes data)
public
returns (uint transactionId)
{
transactionId = addTransaction(destination, value, data);
confirmTransaction(transactionId);
}
value 是多签即将执行的交易所要转移的 ether 数量(以 wei 为单位),data 是该交易的数据。bytes 类型意为 byte[],表示任意数组。因此该交易可以传入任何 data ,以实现任意功能。
几种 data 的写法和功能:
普通转出 ETH,value = 0,data = [](空);
转出Erc20 代币(例如转出 USDT-ERC20),value = 0,data 为 Erc20 transfer 方法的哈希和参数(如图): data = a9059cbb000000000000000000000000859d2cda0310007f050516a9f02559b3755a87cc000000000000000000000000000000000000000000000000000000012a05f200
调用任意合约的任何方法,例如调用 UniswapV2 合约,买入某个 Erc20 代币,data 生成方式和上面类似。
function confirmTransaction(uint transactionId)
public
ownerExists(msg.sender)
transactionExists(transactionId)
notConfirmed(transactionId, msg.sender)
{
confirmations[transactionId][msg.sender] = true;
Confirmation(msg.sender, transactionId);
executeTransaction(transactionId);
}
当 confirm 的人数达到最低(_required)要求,executeTransaction 的内部逻辑将被触发,从而执行第一步用户所提交的逻辑(value 和 data):
function executeTransaction(uint transactionId)
public
notExecuted(transactionId)
{
if (isConfirmed(transactionId)) {
Transaction tx = transactions[transactionId];
tx.executed = true;
if (tx.destination.call.value(tx.value)(tx.data))
Execution(transactionId);
else {
ExecutionFailure(transactionId);
tx.executed = false;
}
}
}
当 executeTransaction 内部逻辑被触发,即完成了多签合约的真正调用,如上所述,value 和 data 可以控制多签执行任意逻辑(转移 ether 或 Erc20 代币等)。
Ownbit 实现多签的逻辑和 Gnosis 不同。可以认为 Gnosis 的实现逻辑为线上方式,而 Ownbit 的实现逻辑为线下方式。
function generateMessageToSign(address erc20Contract, address destination, uint256 value) private view returns (bytes32) {
//the sequence should match generateMultiSigV2 in JS
bytes32 message = keccak256(abi.encodePacked(address(this), erc20Contract, destination, value, spendNonce));
return message;
}
参与签名的参数有:多签合约地址、Erc20代币合约地址(对于转移 ether 使用 0x0)、转移的目标地址、金额、控制重放的合约内部 spendNonce。
对以上参数签名,表示参与方同意对指定合约转移指定金额。
function spendERC20(address destination, address erc20contract, uint256 value, uint8[] vs, bytes32[] rs, bytes32[] ss) external {
require(destination != address(this), "Not allow sending to yourself");
//transfer erc20 token
//uint256 tokenValue = Erc20(erc20contract).balanceOf(address(this));
require(value > 0, "Erc20 spend value invalid");
require(_validSignature(erc20contract, destination, value, vs, rs, ss), "invalid signatures");
spendNonce = spendNonce + 1;
// transfer tokens from this contract to the destination address
Erc20(erc20contract).transfer(destination, value);
emit SpentERC20(erc20contract, destination, value);
}
_validSignature 将对签名的有效性进行验证。验证通过的情况下,相关转币逻辑即被执行。
以上便完成了 Ownbit 多签合约的调用。Ownbit 将不同目的分解到不同的方法中,例如:spend 进行 ether 转移,spendERC20 进行 Erc20 代币转移,spendAny 进行任意功能的调用。
以上两种实现 ETH 多签的不同方式,具有很好的代表性。这也是目前实现 ETH 多签最常用的两种手段。
采用发送交易来表示参与方同意某个花费或调用,避免了复杂的签名计算;
全程线上,具有更好的审计性(参与方的 Reject 的态度也保留在区块链上);
每个参与方都需向线上发送交易,多次花费手续费,不经济;
每个参与方所花费的手续费不均等,使 confirm 人数刚好等于 _required 的交易将花费更大的手续费以执行 executeTransaction 内部逻辑;
交易逻辑隐藏在 data 里,可欺骗性大;
Gnosis 方法的优点正是 Ownbit 方法的缺点,Gnosis 方法的缺点也是 Ownbit 方法的优点。总体而言,Ownbit 的方法因为其经济性,使用得更多。
Ownbit 和 Gnosis 代表了两种不同实现 ETH 多签的方式,了解它们的原理对理解 ETH 多签有非常大的帮助。
多签资产的安全就在这两三百行代码之间,因此读懂并理解它们是开发和使用 ETH 多签必要的技能,也是对智能合约编程能力的一个提升!
首发于公众号:谈谈区块链 链接:https://mp.weixin.qq.com/s/_FYVLPpAh8rXm1Fnn6FM1Q
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!