深入剖析 Ownbit 和 Gnosis 多签

Ownbit 和 Gnosis 代表了当前主流的两种ETH多签实现方式

近期 Ownbit 多签增长迅速,单单 ETH/ERC20 多签一项,管理的资金总额已经超过了1亿美金。Gnosis 是另一个使用较为广泛的 ETH/ERC20 多签钱包。 Ownbit 和 Gnosis 均通过合约账户实现以太坊多签,但是其实现的逻辑却迥然不同。分别代表了当前两种主流的实现方式,我们通过合约源码来讲解实现原理和各自的优缺点。 ## 查看多签合约源码 首先,我们在 etherscan 上分别选取一个 Ownbit 和 Gnosis 多签地址,并在 “Contract” 页面上查看相应的源码。Ownbit 多签合约名称为:OwnbitMultiSig,而 Gnosis 多签合约名称为:MultiSigWallet。 ![](https://img.learnblockchain.cn/2020/12/18/16082629350522.jpg) Ownbit 多签地址例一:https://cn.etherscan.com/address/0xd1b33369848b005330df37f80554cf441114f39b#code ![](https://img.learnblockchain.cn/2020/12/18/16082629576230.jpg) 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 方法的哈希和参数(如图): ![](https://img.learnblockchain.cn/2020/12/18/16082631366556.jpg) data = a9059cbb000000000000000000000000859d2cda0310007f050516a9f02559b3755a87cc000000000000000000000000000000000000000000000000000000012a05f200 * 调用任意合约的任何方法,例如调用 UniswapV2 合约,买入某个 Erc20 代币,data 生成方式和上面类似。 2. 其他参与方提交 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。 对以上参数签名,表示参与方同意对指定合约转移指定金额。 2. 任意一方(甚至可以是多签参与方以外的其他人)发送 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

近期 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 &lt; _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

区块链技术网。

  • 发表于 2020-12-18 11:57
  • 阅读 ( 1397 )
  • 学分 ( 11 )
  • 分类:以太坊

评论