如何在Solidity中设计模块化智能合约

在本文中,我将描述如何使用称为目标模式的东西来模块化智能合约。使用标准的solidity,您将学习如何使用abi.encodeSelector和target.call(data)重写脆弱、耦合的调用,以清除模块和分离关注点。为此,我们将以HumanityDAO为例 - 以及他们如何使用目标模式将治理与注册表分开。

HumanityDAO:介绍

HumanityDAO是一个DAO,其目的是人类验证。维护一个说“你是真的”的注册表。它既能让人安心,又在对抗Sybil攻击方面也非常有用。在我目前的工作中,我正在 使用注册表构建像分散式reddit这样的东西,你可以捎带其他DAO所做的工作来打击垃圾邮件(作为一个基本的例子)。一旦我们开始采用这些模式,您就可以看到整个生态系统如何蓬勃发展。

但除了HumanityDAO是一个伟大的dapp,它也是我看到的模块化Solidity设计的第一个例子。但是在这种情况下模块化指的是什么?我们如何实现它?

HumanityDAO中的模块

HumanityDAO维护一个您添加和查询的人的注册表,称为HumanityRegology。注册表本身仅对单个映射(address=>bool)公共人员上操作。以下是添加条目的主要功能:

function add(address who) public {
    require(msg.sender == governance, 

    "HumanityRegistry::add: Only governance can add an identity");
    require(humans[who] == false, 

     "HumanityRegistry::add: Address is already on the registry");

    _reward(who);
    humans[who] = true;
}

治理是我们将要研究的下一个模块,它涉及提案和投票。这是基本流程:

1. Propose(address target, bytes memory data)开始一个新的提议。它会创建一个Proposal,将其添加到列表中,然后发出一个事件。

2. 用户调用voteyes(uint proposalid)和voteno(uint proposalid)在通过/失败提案时使用其令牌。

3. 投票期过后,可以调用finalize(uint proposalId)。如果proposal.yesCount> proposal.noCount,则使用数据调用target

为了简单起见,最后一步我将在本指南中引用为目标模式。这不是一种新的编程模式.

EVM:调用合同,发送消息

简要说明 - 以太坊合约在EVM中执行,这是一个消息传递执行环境。因此,当您向地址发送10个wei时,您发送的消息显示{from:me,to:0x123456789,value:10}。当我们在合同上执行调用时,除了附加数据之外我们也是这样做的 - 所以现在消息看起来像{from:me,to:0x123456789,data:“0xcafebabe”,value:10}。这是什么数据?嗯,这是根据Solidity应用程序二进制接口(或简称ABI)编码的调用。

消息构造很好,因为它意味着发送以太和调用智能合约没有什么不同。将钱汇入用户的钱包地址,并将钱汇入合同,看起来一样。唯一的区别是,合同的地址不是从公钥生成的(而是从nonce生成)。

因为我们在区块链中,而且我们更愿意以不变的方式进行引用,所以智能合约中的方法由它们的选择器引用。选择器只是函数签名哈希的一部分。

target模式

目标模式非常简单,但尚未被广泛使用,因为Solidity具有这些特殊名称。它也可以称为动态调度(Ruby,Obj-C),回调(JS),无论你想要什么; 

它基本上包括:

· 你正在构建的模块,例如治理
· 你正在实施的事情,例如注册表
· 目标整合点- 即传递提案将添加到注册表

我们将从HumanityDAO的例子中学习如何实现这一目标。

综上所述,我们的目标是:在通过治理投票之后,在注册表中添加一个条目。

在我们将它们组合在一起之前,下面是两个难题,治理和注册模块:

contract Registry {
    mapping (address => bool) public humans

    function add(address who) public {
        require(msg.sender == governance, "HumanityRegistry::add: Only governance can add an identity");
        humans[who] = true;
    }
}

contract Governance {
    function propose(address target, bytes memory data) 

      public returns (uint) {

        uint proposalId = proposals.length;
        Proposal memory proposal;
        proposal.target = target;
        proposal.data = data;
        proposals.push(proposal);
    }

    function vote() public { /* ... */ }

    function finalize(uint proposalId) public {
        Proposal storage proposal = proposals[proposalId];
        require(proposal.result == Result.Pending, 

        "Governance::finalize: Proposal is already finalized");

        if (proposal.yesCount > proposal.noCount) {
            // Call the target contract with the data
            proposal.target.call(proposal.data);
        }
    }
}

我们可以使用Registry.add添加到注册表中。治理的基础是提交一份提案,对提案进行投票,然后调用finalize并通过/失败。如果提案通过,我们将数据中的calldata调用target合同。

那么我们如何构建这个calldata呢? 你会认为这是非常复杂的,即Solidity

不! 我们可以通过一个名为abi.encodeWithSelector的东西来做到这一点:

bytes memory data = abi.encodeWithSelector(registry.add.selector, who);
governance.propose(address(registry), data)

就像那样,目标被设置为注册表合同,并且看起来像Registry.add(who)的调用被编码并存储在提议中。 这真的很简单。

使用wrapper

还有一件事 - 因为我们已经在谈论良好的设计和可组合性 - 我建议在一个单独的合同中用一种方法wrapper这个调用。 这个wrapper智能合约是一个可以在前端调用的简单方法。

我们将建议的代码流命名为什么,并可能将某人添加到注册表中? HumanityDAO称之为HumanityApplicant:

contract HumanityApplicant {
    Governance governance;
    Registry registry;

    constructor(Governance _Governance, Registry _Registry) {
        governance = Governance(_Governance);
        registry = Registry(_Registry);
    }

    function addToRegistry(address who) {
        bytes memory data = abi.encodeWithSelector(registry.add.selector, who);
        return governance.propose(msg.sender, address(registry), data);
    }
}

结论

就是这样! 使用此功能,您可以轻松获取治理合同并在您自己的设计中使用它。

分享到:

相关文章