深入剖析 Ownbit 和 Gnosis 多签

  • 谈国鹏
  • 更新于 2020-12-18 11:57
  • 阅读 1005

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 个人同意的情况下,可以花费该笔资产。在以太坊中,一个地址(私钥)代表一个人。如何表示你同意花费某笔资产?有两种方式:

  1. 用你的私钥对相应的花费(金额、目标地址等等)进行签名,并给出签名结果;

  2. 用你的私钥发送一笔以太坊交易,去调用某个特定接口,并给予特定参数;

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 实现多签逻辑

Gnosis 实现多签逻辑的过程如下:

  1. 任意一方通过 submitTransaction 方法提交交易,得到一个交易号(transactionId,该交易号并非我们常见的交易哈希,而是一个自增长的 uint256):
    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 生成方式和上面类似。

  1. 其他参与方提交 ETH 交易,调用合约的 confirmTransaction 方法,来表示他们对某个交易执行的认可:
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 实现多签逻辑

Ownbit 实现多签的逻辑和 Gnosis 不同。可以认为 Gnosis 的实现逻辑为线上方式,而 Ownbit 的实现逻辑为线下方式。

  1. 相关参与方(满足 _required 个数)线下对即将执行的交易进行签名(所谓线下,即这个过程不需要向以太坊发送交易),生成签名结果(r、v、s):
  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。

对以上参数签名,表示参与方同意对指定合约转移指定金额。

  1. 任意一方(甚至可以是多签参与方以外的其他人)发送 ETH 交易,调用合约的 spend 或 spendERC20 方法,并将以上签名结果作为参数传入:
  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 多签最常用的两种手段。

Gnosis 方式的优点:

  • 采用发送交易来表示参与方同意某个花费或调用,避免了复杂的签名计算;

  • 全程线上,具有更好的审计性(参与方的 Reject 的态度也保留在区块链上);

Gnosis 方式的主要缺点:

  • 每个参与方都需向线上发送交易,多次花费手续费,不经济;

  • 每个参与方所花费的手续费不均等,使 confirm 人数刚好等于 _required 的交易将花费更大的手续费以执行 executeTransaction 内部逻辑;

  • 交易逻辑隐藏在 data 里,可欺骗性大;

Gnosis 方法的优点正是 Ownbit 方法的缺点,Gnosis 方法的缺点也是 Ownbit 方法的优点。总体而言,Ownbit 的方法因为其经济性,使用得更多。

结语

Ownbit 和 Gnosis 代表了两种不同实现 ETH 多签的方式,了解它们的原理对理解 ETH 多签有非常大的帮助。

多签资产的安全就在这两三百行代码之间,因此读懂并理解它们是开发和使用 ETH 多签必要的技能,也是对智能合约编程能力的一个提升!

首发于公众号:谈谈区块链 链接:https://mp.weixin.qq.com/s/_FYVLPpAh8rXm1Fnn6FM1Q

点赞 1
收藏 2
分享
本文参与登链社区写作激励计划 ,好文好收益,欢迎正在阅读的你也加入。

0 条评论

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