译者:谢晗剑
金融是风险管理的艺术。无论资产本身还是操作过程都可能存在风险。资产具有价格,而价格是其内在价值和综合风险的反映。对资产进行评估时,对风险的评估也必不可少。操作风险主要来自于易出错且可被收买的“人”。因此,无论是对于基于传统资产的传统金融,还是对于运行在原生加密资产上的新兴去中心化金融(又名 DeFi)来说,对资产和操作风险的评估都是金融的核心。
风险控制是关键
加密资产的风险既包括政策变化等外部风险,也包括设计缺陷和实施错误等内部风险。在以太坊上,原生代币就是 ETH,而非原生代币就是我们所谓的 ERC token。ERC token 是指符合 ERC20 标准及其衍生标准(例如 ERC721 和 ERC777 等)中任何一种的 token。原生资产的风险会低于非原生资产的风险,因为后者除了受到以太坊客户端漏洞的影响外,还会受到智能合约漏洞的影响。对于 ERC token 和 DeFi,智能合约中的漏洞是最受关注的,因为 DeFi 作为一个系统,是由来自世界各地的不同的开发者们创建的无数智能合约交织在一起的复杂网络。我们把由智能合约漏洞引起的风险称为智能合约风险。
针对智能合约的漏洞,人们做了大量研究,我们也发现了很多防御的方法。如果您有兴趣,这里有一份不错的调查报告[1]。然而,众所周知,找到并修复所有较重要的智能合约(或者所有程序)中的漏洞是不可能的。我们生活在一个充满通信错误和随机(变化)的世界中,每一步都可能会导致“失真”:我们无法将头脑中的想法写成精确的说明书,同样地,我们也无法将这份说明书毫无瑕疵地呈现出来。
如果这就是残酷的现实,那么我们在加密资产的实现中,不仅要考虑到主动防御和被动防御,同时还应该考虑到风险控制,这样当一些不好的情况发生时,能够将损失降到最低,避免一个微小的错误最终演变为黑天鹅事件。智能合约可以通过多种设计模式来实现风险控制,最重要的一种(我认为)是应用程序状态的去中心化,因为应用程序状态的中心化会放大由智能合约漏洞造成的破坏。我将在接下来的文章中进一步阐明这一点。
ERC token 的风险
ERC token 的几种标准从表面上看可能有所不同,但都具有一些共同的特征。ERC token 的基本模式是使用 token 合约来管理 token 帐本,用户通过与 token 合约的交互来实现发行、转移或销毁 token。token 账本的所有记录都存储在 token 合约中,而合约本身则只是以太坊上的一个账户。
例如,假设有一种 ERC token 叫 “cup”,Alice 拥有 100 个 cup,Bob 拥有 50 个 cup。该 token 的帐本位于地址/帐户为 0x1234 的以太坊智能合约上。这个 token 合约 0x1234 会维护一个内部数据库,并在其中储存诸如 “Alice 拥有 100 个 cup” 和 “Bob 拥有 50 个 cup”之类的记录。当 Alice 想转 30 个 cup 给 Bob 时,她就向合约 0x1234 发送了一条带有签名的消息,内容为 “请转 30 个 cup 给 Bob”,然后合约 0x1234 将会验证该消息是否确实来自于 Alice,然后修改其内部数据库,更新为 “Alice 拥有 70 个 cup” 和 “Bob 拥有 80 个 cup” 的相关记录。
这里存在的问题是,所有的逻辑和状态都保存在这一个合约 0x1234 中。Alice 和 Bob 无法直接访问他们自己的记录,因为这些记录由合约 0x1234 保管。持有该 token 的所有用户都需要与该合约进行交互,而管理其 token 的唯一方法就是通过向合约 0x1234 发送消息。
换句话说,token 合约是一切的中心。任何系统的中心都是攻击者最有利可图的攻击对象。中心的任何问题都会影响所有用户,因为所有 token/记录都由中心保管,并且每个人都必须与之交互。比如,由于整数溢出导致的 “transferFlaw[2]” / “allowAnyone[3]” 漏洞,或由不谨慎的授权导致的 “ItchySwap[4]” 漏洞,攻击者可以在所有者并未与合约交互的情况下,窃取他人的 token。通过 “ownerAnyone[5]” 漏洞,攻击者可以通过控制 token 合约来锁定所有人的 token。在这些例子中,一旦 token 合约被破坏,每个人都将陷入困境。
ERC token 是由 token 合约而非个人用户持有的,这与以太坊的原生 token ETH 有很大的不同。ETH 的余额并不是由任何智能合约存储的,而是由用户直接控制。每个 ETH 所有者都拥有自己的帐户和余额记录。例如,如果在以太坊主链上,Alice 有 100 个 ETH,Bob 有 50 个 ETH,那么 Alice 在自己的账户中记录为“余额 = 100”,Bob 在自己的账户中记录为“余额 = 50”。Alice 的余额只会随着带有她签名的转出而减少,Bob 的帐户也是如此。ETH 的所有权记录分散在多个帐户中,而非集中在单个帐户中。只要他们的私钥是安全的,就没人能从他们那里窃取 ETH。相反,ERC token 的攻击范围则更大,因为攻击者完全可以尝试破坏 token 合约,这比破坏协议本身要容易的多。
不同的内生风险使得 ERC token 和 ETH 成为两种不同类型的资产。在去中心化网络上运行的智能合约与去中心化网络是不同的。中心化的问题是由于 ERC token 在应用层而引起的,这会放大智能合约漏洞的潜在危害,不幸的是,我们知道人类总是会犯错误,所以总是会有漏洞。网络/共识层的去中心化无法解决应用层的中心化问题。
如何降低风险
中心化的现象出现在 ERC 系列的 token 中,是因为它们的状态并非是以太坊编程模型中的一等公民(first-class citizen)[6]。在以太坊中,状态是代码的附件,不能直接引用和比较。当然将具有相同验证规则(比如,相同 token 的记录)的状态放在同一个合约中是很自然的事情,但是,这就会导致该合约在某种程度上变成中心化的合约。这就是在以太坊编程模型中出现的结果。
但是在 CKB 上,由于状态是一等公民,因此正好相反。状态是用户可以直接把玩的对象,而代码只是状态的附件。我们可以自然地将使用相同验证规则的状态进行比较和分组,即使这些状态由不同的用户直接持有。
在 CKB 上,我们将非原生代币称为 “用户自定义 token” 或者是 UDT。对于给定的 UDT,资产定义(代码)和资产记录(状态)是分开的,不同用户的记录(地址)也是分开的。资产定义描述了 token 的逻辑,比如”发行上限是 1 百万“或者”Bob 可以发行新的 token“,而资产记录则为”Alice 拥有 100 个 token“之类的信息。
资产定义是一个合约,由 token 发行者创建,并存储在发行者所拥有的 Cell 中(资产定义 Cell,Asset Definition Cell),而资产记录则保存在用户自己的 cell 中,并且所有的资产记录均使用相同的“类型脚本”(type script)。
每个用户都使用他/她自己的 Cell 来存储他/她自己的 token 记录。这些 token 记录共享资产定义 Cell 中定义的相同验证规则。通过这种结构,资产记录都以去中心化的形式存储着。
如上图所示,Alice 持有的 token 存储在 Alice 自己的 cell 中,并且(这个 cell)由她自己的“锁脚本”(lock script)保护着,目前默认采用的是 Secp256k1。即使资产定义存在问题,攻击者也无法修改 Alice 的资产记录,因为这样做需要 Alice 的私钥。因为 token 是由 Alice 直接持有的,因此攻击者无法绕开 Alice 设定的锁。通过 token 状态的去中心化,我们可以有效控制由资产定义的漏洞所造成的损失。
但是这里仍然有可能存在影响每个人(资产)的漏洞:比如,资产定义中可能存在一个漏洞,这个漏洞允许任何人发行比预期更多的 token。UDT 的好处是,首先,消除了大部分的漏洞;其次,资产定义 Cell 受到锁(脚本)的保护,该锁(脚本)自然也可以是 token 发行中身份验证逻辑的一部分。通过协议设置身份验证机制是非常容易的。这种对于授权和业务逻辑的解耦是一种非常棒的工程实践,也是 CKB 中默认的做法。UDT 会比 ERC token 有更好的去中心化属性,因此 UDT 的智能合约风险要低于 ERC token,当然还是高于原生代币。
如果您也对 UDT 感兴趣,在 Nervos Talk上已经有了一些讨论[7],欢迎参与。
安全的 Nervos DAO
应用程序状态的去中心化也有助于 DeFi 程序的设计。Nervos DAO[8] 是 CKB 上第一个 DeFi 应用。它是一个智能合约,用户可以用相同的方式与 CKB 上任意一个智能合约进行交互。Nervos DAO 的其中一个功能是为 CKByte 的持有者提供一个抗(二级发行)稀释的对策。通过将 CKByte 存入 Nervos DAO,CKByte 持有者将获得相应的二级发行奖励,这保证了他们持有 CKByte 的占比只会受到基础发行硬顶的影响,就像比特币一样。在撰写本文时,DAO 中已经存入了超过 10 亿个 CKByte[9],并且这一数字还在不断上升,这不就是攻击者苦苦寻找的攻击对象吗?对此,我们需要担心吗?
大可不必!因为 Nervos DAO 中锁定的 CKByte 并没有汇聚在同一个智能合约内,它仍然由不同的用户持有!当用户想要将 CKByte 存入 Nervos DAO 时,他/她通过选择 cell(CKByte 的 UTXO 模型),然后将这些 cell 的 type script 的引用设置为
0x82d76d1b75fe2fd9a27dfbaa65a039221a380d76c926f378d3f81cf3e7e13f2e,这串数字会指向 Nervos DAO 脚本[10]。这些 cell 的 lock script 将保持不变。因为 Nervos DAO 的 type script 和 lock script 是分离的,因此攻击者在没有用户允许的情况下,永远不可能使用这些存储中的 CKByte。
如果要提取已锁定的 CKByte,则必须要提供 lock script 对应的证明(签名)。这个验证过程不是通过任何智能合约,而是由 CKB 网络来保证的,并且没有其他替代方案。这里,Nervos DAO 的脚本是为了保证用户退出 DAO 时,能够通过计算得到正确的补偿,而并非是为了持有(用户的)资金。
对 DeFi 的影响
目前很难定量分析智能合约风险对于 DeFi 的影响,但是我们可以从 Nexus Mutual[11] 提供的保险产品中获取一些线索,这是一种去中心化的替代保险。它的第一个产品 SmartContractCover[12] 可以让用户购买任何一个智能合约的保险,这样一来,如果该智能合约被黑客攻击,那么用户就可以获得对应的赔偿。Smartcontractcover 的保费由市场决定,并与保险期限和保额呈正相关。根据此文[13],Nuo 中 1000 个 DAI 对应 90 天的保险费用为 6.41 个 DAI,而 Uniswap 中 1 个 ETH 对应 365 天的保险费用为 0.013 个 ETH。基于这些数据,我们可以大致估计出,目前智能合约的年化风险成本约为 2%。
这 2% 的成本是由于智能合约风险所导致的市场效率低下而引起的,因此改进智能合约编程模型就显得异常重要。一个不同的编程模型可以降低智能合约的风险,并为我们带来一个更高效的 DeFi 市场。
感谢 Haseeb Qureshi,Cipher Wang 和 Christopher Heymann 对本文提出的反馈意见。
原文链接:
https://medium.com/nervosnetwork/the-smart-contract-risk-in-defi-c28e53b92f03
引用链接:
[1]. https://arxiv.org/abs/1908.04507
[2]. https://blog.peckshield.com/2018/04/28/transferFlaw/
[3]. https://blog.peckshield.com/2018/05/23/allowAnyone/
[4]. https://blog.peckshield.com/2019/09/27/ItchySwap/
[5]. https://blog.peckshield.com/2018/05/03/ownerAnyone/
[6]. https://en.wikipedia.org/wiki/First-class_citizen
[7]. https://talk.nervos.org/t/discussion-on-udt-standard-evaluation-criterion/3774;
https://talk.nervos.org/t/approach-to-designing-a-user-defined-token-standard-on-ckb-part-1/3855;
https://talk.nervos.org/t/proposal-minimal-udt-implementation-and-extensions/3932;
[8]. https://github.com/nervosnetwork/rfcs/blob/master/rfcs/0023-dao-deposit-withdraw/0023-dao-deposit-withdraw.md
[9]. https://explorer.nervos.org/nervosdao
[10]. https://github.com/nervosnetwork/ckb-system-scripts/blob/master/c/dao.c
[11]. https://nexusmutual.io/
[12]. https://nexusmutual.io/pages/SmartContactCoverWordingv1.0.pdf
[13]. https://medium.com/nexus-mutual/smart-contract-cover-is-now-live-91b3015f99eb