eosDAC 智能合约详解

到底发生了什么?

前几天有人问我关于 eosDAC 的事,我们正在开发的这套我们称之为 DAC 工具包的东西,到底有什么特别之处。它与支持其它组织治理和运营的中心化的解决方案有何不同?它如何保证安全?智能合约集是如何设计和开发的,可以确保没有任何一个实体能够通过智能合约损害 DAC?

已经有几篇文章和博客讨论了 DAC 的核心理念以及如何使用本文中的代码来运营一个 DAC。在这篇文章中我将重点关注智能合约正在做什么,以及它们如何协同工作以实现安全的自主治理。所有的代码都是开源的,可以在 Github 上查看/使用/ fork,但并不是每个人都能读懂 C++。

一个首要考量是我们所有的智能合约都可以通过简单的代码复制即在其它 DACs 进行重用。这是我们一开始的核心信念之一,这也是我们的设计决策比单一用例更复杂的原因。我们未来的目标是提升可重用性,以便使多个 DAC 可以使用我们已安装的智能合约实例,又能够根据自己的目标来调整设置,且无需维护自己的代码。通过这种方式,他们可以获得 eosDAC 团队的功能更新和常规错误修复等更新。

代码合约包括代币合约(eosdactokens),一个选举和托管人管理合约(daccustodian),一个负责管理所有工作人员提案及协调付款的工作提案合约(dacproposals),以及一个单独的托管账户(dacescrow),用于为正在进行中的工作人员提案保管资金,以保护 DAC 和工作人员免受可能的工作损失。

EOSDACTOKENS

这就是一切开始的地方。EOSDAC 代币正是由这个智能合约产生。它起源于用于 EOS 代币 eosio.token 主合约代码的复制,这个主合约很可能是所有 EOS 区块链上运行的所有代币的起点。我们在此合约中添加了一些功能,以满足我们启动 DAC 和初始空投的需求,包括以下内容:

能够在锁定状态下创建代币

这样做的目的是,当我们对早期基于以太坊网络的 EOS 代币的持有者进行初始空投后,回到 2018 年 6 月,我们可以对所有接收账户的余额进行全面测试,这样在用户可以交易代币之前,确保他们与以太坊网络快照的期望余额相同。我们非常重视测试 :)。

EOSDAC 是最早在区块链上进行空投的代币之一,因此有许多未知数。一旦空投完成并通过测试,我们就可以解锁代币并允许转账。更重要的是,一旦解锁了代币,就无法再锁定它,因为此功能可能使代码创建者能够通过集中控制代币的流动性的方法来操纵价格。

会员条款及确认

作为 DAC 的一部分,同意使用条款被我们视为 DAC 智能合约的基本核心功能,因此将此功能包含在代币合约中非常重要。当用户同意这些条款后,智能合约开始执行 memberreg 操作,然后用户就注册成为会员。

要执行这个操作,用户需要提供他们实际同意的条款的检验和(checksum),合约将确保这些检验和与最新条款的已知检验和相匹配,然后才能确认其条款和会员资格。这个检验和逻辑是通过前端用户界面由终端用户生成,并提供加密见证,表明用户同意一组特定术语,并准备好让其他人添加到他们的用户界面并与合约代码交互。只要他们能够提供与已知条款和条件相同的检验和,合约就能够成功运行 memberreg 操作。虽然这似乎是一种同意条款和条件的冗长方式,但我们采用这种方式,是因为它就像是在说“我同意这些条款和条件”,而不是向用户询问“你是否同意最新的条款和条件”而用户回答“是”或“否”。我们认为后者不够健壮,它并不能确保用户不会意外地接受错误的术语或避免用户界面使用了过期的合约。

为了节省所有最终用户的 RAM 占用,合约仅一次性存储了确认条款哈希的引用,而不是为每个用户都存储确认条款的副本。这意味着每个用户只需要存储确认条款的版本(8个字节)而不是整个哈希串(32个字节),因为链上的 RAM 非常昂贵,每节约一点都很有帮助。

这个会员接受最新条款的状态在整个 eosDAC 智能合约中被反复引用,以确保用户无法在未同意最新条款的情况下对 DAC 执行操作,尤其是有将来更新条款的情况。

DACCUSTODIAN

本合约管理每个选举周期结束时的所有抵押、提名、投票、选票统计和托管人任命。此合约操作的最终目标是用来管理 DAC 管理帐户 (dacauthority) 的权限,因为管理帐户具有在 DAC 中执行任何操作的预定义权限,包括更改代码和转移资金。这些许可的操作将利用 EOSIO 协议软件中提供的内置复杂权限管理工具来处理各种多签交易。要更改选举过程中的参数,可以用 updateconfig 操作来更新合约上的配置对象。 本合约的功能可分为以下几个部分:

抵押

在成功提名为候选人之前,会员必须首先使用对此帐户代币合约的 transfer 操作将 EOSDAC 代币作为抵押数额进行转账。转账的 memo 必须设置为此合约的账户名称(例如“daccustodian”),以便可以将这笔转账识别为一个抵押交易,且所需抵押的数额可由配置对象中的 lockupasset 字段配置。成功抵押后,该候选人的合约中将会有一定的抵押数额,可用于实际的提名操作。

候选人提名 - nominatecand

一个有效会员要当选为 DAC 的托管人,首先要使用 nominatecand 操作来提名为候选人。这需要提供提名帐户的帐户名称,且需要支付一定数量的 EOS。合约将检查用户的会员身份有效且已抵押了足够多的 EOSDAC 代币。它还检查已支付的金额是否超过了配置中的 requested_pay_max 的数量。如果所有这些条件都满足,合约将该用户添加为在下面投票操作中可投票的候选人。

投票 - votecust

投票是通过 votecust 操作进行的,并且需要投票人账户和投票人将要投票支持的候选人名单。投票遵循以下规则:

  • 投票需要有效的会员资格并同意当前的会员条款。
  • 每次投票的权重由投票人在其账户中的 EOSDAC 余额确定。(目前不需要为投票进行抵押。)
  • 一旦投票完成,投票将持续有效,直到投票人更改投票。这意味着如果帐户余额降至 0 然后再增加余额,或其中一个候选人放弃提名后再次提名,此投票仍将有效并适用于投票统计。
  • 所有由投票人投票支持的候选人根据 EOSDAC 余额获得相同的投票权重。
  • 投票人可以选择支持的候选人人数有一个上限,并取决于配置设置。
  • 要取消投票,投票人需要清空已选择的候选人列表并再次投票。
  • 在投票账户中投票和转移代币将对每个有效投票的权重产生直接和即时的影响,但对投票权重来说,重要时刻是当 newperiod 操作被调用的时候(下文解释)。

新选举周期 - newperiod

对于每个有效的候选人,如果有正在转账状态中的 fromto 账户的活跃投票,投票权重将基于代币合约中的 votecust 操作或 transfer 操作进行持续的跟踪。在连续跟踪这些值的情况下,新选举周期(在 newperiod 合约运行时)只需要对候选人的得票数进行快照并按投票最多进行排序。候选人的数量可通过配置中的 numelected 字段进行配置。为了成功运行 newperiod 操作,还需要进行其它检查,包括:

  • 确保初始投票人数超过 initial_vote_quorum_percent 的配置。
  • 确保在随后的选举中投票人数超过 vote_quorum_percent 的配置。
  • 确保调用 newperiod 之间的时间间隔超过 periodlength 的时间延迟。

如果所有这些检查点都满足,则执行以下操作:

  • 托管人工资按照当前托管人 requestedpay 金额的中位数进行分配。
  • 根据下一个周期选票累计排名权重任命新的托管人。
  • 更新 DAC 账户的操作权限,把新任命的托管人加进去。
  • 保存当前时间,并作为将来调用 newperiod 的时间引用,以确保在下一个周期不会过早调用。
  • 对于当选为托管人的每位候选人,他们抵押的代币将在此时被锁定,以表示他们利益共享、风险共担。一旦他们不再当选,且超过了在配置中由 lockup_release_time_delay 指定的锁定期,他们将能够赎回他们的代币。

其它操作:

除了上面解释的常规情形之外,本合约还支持其他操作,包括:

  • 候选人退出 (withdrawcand):此合约将由现有候选人调用,包括想要从下一个选举期间退出选举的现任托管人。他们的代币将保持抵押状态,并一直保持锁定,直到释放时间到期。这个操作的一个很好的例子可能是托管人去度假了,并计划很快会回来(这就是保持赎回功能独立的原因)。另外,此操作可用于其他任何原因,以便候选人自愿退出 DAC 的运作。
  • 除名候选人 (firecand):当前现任候选人可以通过多签验证账户调用此合约来移除一个行为不端的候选人。如果候选人的抵押的代币未锁定,此操作有一个选项可以锁定它。
  • 除名托管人 (firecust):与 firecand 类似,此操作将由其他现任托管人来执行,来移除一个现任托管人。这将从现任托管人组中移除这个托管人,并将其删除为未来选举的潜在候选人,并用下一个最高得票的候选人替换,最后更新帐户权限把新当选的托管人加进去。
  • 托管人辞职 (resigncust):此操作必须由想要辞去托管人资格的现任托管人来执行。这将使他们从符合条件的候选人中删除,并用下一个最高得票的候选人替换。
  • 更新期望的工资 (updatereqpay):候选人可以更新他们作为托管人期望的工资。为避免在当前任期变更托管人的工资,此更新将在下一个任期生效。
  • 申请支付 (claimpay): 这是由现任托管人发起的操作,以便可以收到应支付给他们的工资。鉴于我们生活在传统的法律体系中,付款是通过服务公司进行的,有关法律原因的更多解释超出了本文的范围。从合约的角度来看,这是为他们作为托管人为 DAC 提供服务而应获得的报酬。在操作执行后,托管人应被视为已支付工资。
  • 更新配置 (updateconfig): 合约代码里有几个可配置选项,无需重新编译和部署代码即可自定义使用。未来的计划是将允许不同的 DAC 使用相同的已部署代码进行操作,但又都拥有各自的自定义配置。通过在以下合约选项中设置新的配置对象来进行自定义设置:

    • lockupasset: 申请竞选的每位候选人锁定的 EOSDAC 代币数量。
    • maxvotes : 每位会员可以投票支持的候选人的最大人数。默认值为 5。
    • numelected : 每届选举的当选托管人人数。
    • periodlength : 以秒为单位的每届任期的长度。用于避免调用 newperiod 操作提前进行选举。默认值为 7 天。
    • authaccount : 为当选托管人设置权限的控制帐户。
    • tokenholder : 保管 DAC 资金的合约。这是托管人工资的来源。
    • serviceprovider : 此合约将用作 DAC 的服务提供商帐户。 被用于为托管人及提交提案的工作人员发放工资。
    • should_pay_via_service_provider : 如果设置为 true,则合约将通过服务提供商来发放所有工资,而不是直接付款。
    • initial_vote_quorum_percent : 触发首届托管人所需的投票的代币数量。
    • vote_quorum_percent : 在达到初始阈值后允许设置一组新的托管人所需的投票的代币数量 - 第2个选举周期及以后。

      需要一定数量的托管人来批准 DAC 智能合约上不同级别的需要身份验证的操作:

    • auth_threshold_high
    • auth_threshold_mid
    • auth_threshold_low
    • lockup_release_time_delay : 可以使用赎回(unstake)操作将之前锁定抵押(stake)的代币退回给候选人的时间延迟。
    • requested_pay_max : 托管人可以申请的最高工资。

DACPROPOSALS

该合约负责管理与 DAC 相关的工作人员提案。它再次被构建并支持可配置性,而不仅仅是为了满足我们在 eosDAC 的即时需求。

一般的构想是,潜在的工作人员将发起提案做一些他们期望可以提升 DAC 价值的工作,并获取一定数量的基于 EOS 代币的酬劳。该提案将由现任托管人投票批准启动,之后还将确认工作完成,这些操作将触发向提案的工作人员支付薪酬。

发起提案 createprop

发起提案的工作人员将创建提案并将其提交给区块链,以供现任 DAC 托管人进行评估和投票。提案需要包括以下内容:

  • title (字符串): 用于标识提案
  • summary (字符串): 关于工作人员提案的用处的简要概述
  • arbitrator (EOS 账号名称): 独立仲裁员的账户名称,可在工作人员提案完成后要求协助解决争议。
  • pay_amount (EOS 资产): 基于 EOS 代币的数额,作为支付给工作人员的的工资
  • content_hash (检验和哈希值): 一个内容哈希值,用于确保提案通过后不会修改存储在链下的提案内容。这允许在保证数据完整性的同时,更详细的提案细节不用存储在链上。

对于每个提案,最小内容数据需要存储在合约的状态中,而仅通过事务日志传递以实现数据的完整性。只保存了帐户和付款数据,以便在此合约的后续操作中使用。

对提案进行表决 voteprop

一旦提案被创建,将等候现任托管人进行投票表决,表决结果可以是 ‘提案通过(proposal_approve)’ 或 ‘提案拒绝(proposal_deny)’,该提案表决所需的”是”的投票数可以在合约中进行配置。如果是已有提案,也可以用取消 cancel 操作来重新对提案进行改进,并根据托管人的反馈意见重新修改并提交,直到提案准备就绪并表决通过。

提案通过后开始工作 startwork

对于一项有足够支持票的提案,提案人可以调用这一操作来确认他们将同意按照商定的条款、款项等既定条件开始执行提案。此时,商定的款项将被转入托管账户,以确保工作完成后由托管人或者商定仲裁员确认后,该款项将可以支付给提案人。

在一个托管账户中锁定资金是一个关键步骤,这样可以保护工作人员免受那些可以撤销已通过提案的恶意托管人的危害,因为工作人员在这个提案上还有一个可信的仲裁员可以能够支付工资。

工作完成信号 completework

在工作人员完成工作后,他们会向托管人发出信号,表示工作已按计划完成,托管人可以在投票之前评估工作情况,然后使用 voteprop 进行投票,但这次使用 claim_approveclaim_deny 来表示不同的投票结果。

为已完成的工作申请付款 claim

如果基于当前配置下托管人对已完成的工作投了足够多的赞同票,则提案人就可以调用该操作,该操作将触发向工作人员发起代币转账进行支付。在 eosDAC 的实践中,资金被发送到服务公司账户后直接作为服务薪酬支付给工作人员。

合约配置 updateconfig

该合约可以配置各种选项,包括:

  • service_account (EOS 账号名称)
  • proposal_threshold 提案参与投票表决所需的投票数。
  • proposal_approval_threshold_percent 批准提案所需的赞同票百分比。
  • claim_threshold 确认提案完成所需的赞成票的票数。
  • claim_approval_threshold_percent 确认提案完成所需的赞成票的百分比。
  • escrow_expiry 在创建的托管交易上设置的过期时间(秒数)。其默认值为30天。

DACESCROW

该合约负责为提案工作人员保管担保资金,直到当选托管人或协议仲裁员向接收方发放资金或者托管时限到期,这样才能将资金返还给发送方。这么做的意图是锁定该合约以阻止恶意托管人修改代码。 这可以通过在部署合约代码后删除帐户的 owner 及 active 密钥来实现。将这个合约与其它合约代码进行代码级别的简单分离,是为了追求不可更改性。我们希望这个代码永远不需要修改。 在不幸(并且希望永远不会)的情况下,需要修改此代码,托管人需要从当前区块生产者获得必要的承诺以可以重置此帐户的 owner 密钥。

初始化托管交易 - init

必须初始化托管交易,并设定所有必需的字段,包括发送方、预期接收方、到期时间、仲裁方,最终转账操作的 memo。还有一个可选的外部密钥,可用作交叉合约参考密钥,而不仅仅依赖于内部自动递增密钥,因为这可能会导致即时密钥冲突。

转账资金进行托管 - transfer

托管资金需要转账给托管合约,使用了比较常见的直接从大多数基于 EOS 的代币合约复制而来的普通的 transfer 操作。此合约的代码依赖于内置通知,即转账操作指向转账帐户的发件人和收件人。当托管合约收到转账通知时,transfer 操作将验证发件人是否有空的托管记录,并将转账到该托管记录中的款项分配给托管合约代码中的其他操作以供其稍后处理。如果在托管执行转账操作之前调用了 cancel 操作,则可以删除初始托管记录。

批准托管或取消批准托管 - approveunapprove

一旦托管完成初始化并执行转账操作,下一步将由发件人、指定的仲裁员或接收者批准托管。这三个账户中的任何一个都需要另外两个同意才能执行 claim 操作。随后可以调用 unapprove 操作来删除相关角色的已有批准。

申请已批准的托管付款 - claim

申请操作只能由预期的接收方执行才能进行托管付款,并且只有在托管记录处于正确的批准状态下才能成功。此时,托管款项将转账到指定的服务公司账户,以便可以将付款发送给预期的接收方。

注意: 加入服务公司这一步并不是出于技术原因,而是由于要满足传统法律世界的法律要求。尽管我们希望在加密安全的智能合约环境中执行所有操作,但传统世界尚未做好准备 :( 。

到期后退款 - refund

因为托管账户的资金必须被锁定一段时间,且到了过期时间后才能释放。但如果没有得到发送方或仲裁员的完全批准,账户里的资金则可能会被永久锁定。refund 操作提供了一种机制,到了过期时间后可由并仅由发送人调用。然后托管款项将会被转回给发送人,并删除托管记录以防止出现双重退款。

返回