第1部分:为什么我们需要一种新的智能合约语言?
2018年6月17日
Amrit Kumar发表于Zilliqa博客,Rita译
在2018年5月23日举办的Zilliqa重要见面会上,我们演示了用Scilla编写的一个众筹智能合约(视频链接:https://youtu.be/_7Rv1Q5exbE?t=4403)。Scilla是我们为Zilliqa开发的全新智能合约语言,其论文链接为:https://arxiv.org/pdf/1801.00687,它在设计过程中始终把智能合约的安全性放在重要位置,是一种原则语言(principled language)。
Scilla通过在智能合约上强加一种结构,直接在语言层面消除了某些已知漏洞,从而使在Zilliqa上运行的应用程序不易受到攻击。此外,Scilla的原则结构也将使应用程序在本质上更安全。
为了展示Scilla背后的设计原理及其“安全”特性,我们从此篇开始将撰写一系列文章,向大家逐篇介绍Scilla的设计构思。
首先,我们为什么需要建立一种新的智能合约语言呢? 我们旨在解决什么问题?
本篇是这个系列文章的首篇,将通过介绍智能合约的主要痛点 — — “安全性”,来回答这些问题。
安全是关键
在传统软件开发领域,设计新编程语言的目的通常是让开发人员能更轻松地完成某些任务。例如,随着面向对象的编程语言的出现,代码复用变得更加容易;通过使用脚本语言(如Python),自动执行系统级任务变得更加轻松;像Java这样的语言可以使管理内存更轻松;而Go可以使处理线程更方便等等。
尽管智能合约语言在设计原则上与传统编程语言有一些共同点,但它属于特定领域,与传统编程语言主要有两个不同:
一方面,由于区块链具有不可篡改性,因此智能合约是无法更新的。当我们将智能合约与传统软件相比时我们会发现,如果软件中存在漏洞,我们可以修复该软件并发布新版本,而智能合约的漏洞却很难修复,只能通过硬分叉来实现。考虑到智能合约平台上通常附加着大规模的区块链经济,因此智能合约无法更新是一个很大的发展限制。截至2018年6月17日,仅以太坊的市值就约为500亿美元。
另一方面,智能合约在支付计算成本上的燃料机制(gas mechanism)上也区别于传统编程语言。因此,在编写智能合约时,开发者必须确保其中的每个功能都能不受燃料限制按预期运行。不恰当的资源分析可能会导致智能合约代码的一部分因燃料限制而无法执行,从而出现资金卡住的情况,而在传统的软件系统中却不存在这种约束。我们将在本系列文章的后边篇章中再次提到这一点。
因此,确保部署在区块链上的智能合约是无缺陷且安全的尤为重要。智能合约的安全性特别重要,因为它们是运行在拜占庭式的环境中的,每一个合约参与方都可能是拜占庭的即恶意的。例如,一个参与合约的恶意用户可能想要盗取资金;一个矿工可能在一个区块中刻意排序一些交易,以产生一些意想不到的结果;或者最糟糕的情况是用户在调用合约时引发调用其他合约(比如调用一个库合约),而后者却是由攻击者控制的,因此产生恶意行为等。
智能合约安全问题实例
让我们回顾一下过去发现的一些智能合约的安全问题和漏洞。我们的目标是通过明确我们想要解决的问题从而设计一种全新的智能合约语言。
我们在这里有意地选择了一些简单,但又能反应真实世界中如DAO和Parity等攻击中的核心问题的实例,从而确保大家能够更清晰直观地了解问题,而不被一些不必要的复杂性和行话弄得眼花缭乱。
实例1:泄露资金的合约
合约有多种导致资金泄露的方式。例如,合约可能将资金转给非指定的收款人;或者将超额资金转给合法收款人等。
以下展示了对DAO合约的攻击,该攻击者借此盗取了6000万美元。大家可以观察到,该合约有一个状态变量shares。将状态变量视为可由任何函数访问的全局变量。shares维护用户地址和相应份额之间的映射。股东可以调用withdraw()来提取他们的份额。
contract UnsafeContract1{
// Mapping of address and share
mapping(address => uint) shares;
// Withdraw a share
function withdraw() public {
if (msg.sender.call.value(shares[msg.sender])())
shares[msg.sender] = 0;
}
}
如果用户在链外调用withdraw(),那么UnsafeContract1将以良性方式运行。在这种情况下,合约发送一条消息将份额通过msg.sender.call.value()转给用户,然后通过更新下一行中的shares将份额设置为0。
攻击发生的情况是,收款人是一个合约而非用户。当合约调用方调用withdraw()时,被调用者执行msg.sender.call.value()并将执行控制权传递给调用者即合约,在这种情况下可以回调到withdraw()。
请注意,在withdraw()中,只有在if(msg.sender.call.value())终止后,调用方的shares才会更新为0。当恶意合约回调withdraw()时,它实际上是通过强制它停留在if()指令,来防止程序指针更新shares。这允许恶意合约多次提款,直到其燃料费被消耗殆尽。
如果收款人是用户而非合约,那么他将无法回调合约,因此执行将按预期结束。
这种攻击也被称为重入攻击(re-entrancy attack)。
实例2:对关键状态变量的意外更改
合约有状态变量,其中一些可以在合约创建时由创建者实例化,这些在之后是不能更改的,而那些在创建合约时没有实例化的内容可以在之后修改。由于这些变量可以在之后更改,因此如果该变量对合约的安全性至关重要,那么则需要采取适当的谨慎措施。
以下合约模仿了一个发生在名为Parity的多重签名钱包上的攻击,攻击者盗取了3100万美元。请注意,实际的攻击涉及的内容要更广泛,但这个简化版的实例展示了它的核心内容。
合约有一个owner,它不是在创建时实例化的,而是在后来通过函数initowner()进行的。在实例化后,owner可以调用transferTo()并将合约中指定的_amount转给特定的_recipient。
contract UnsafeContract2{
/* Define the contract owner*/
address owner;
/* This function sets the owner of the contract */
function initowner(address _owner) { owner = _owner; }
/* Function to transfer the funds in the contract */
function transferTo(uint _amount, address _recipient) {
if (msg.sender == owner)
_recipient.transfer(_amount);
}
}
显然,owner是一个关键的状态变量,应该以适当的方式实例化。不幸的是, initowner()函数允许任何包括恶意的用户调用,并将owner设置为他选择的任何地址。一旦owner被设置,owner就有可能盗取资金并将其转给任何收款方recipient()。
实例3:终止合约
我们来扩展一下前面的实例,一旦设置了关键变量owner,攻击者就可能调用除transferTo()之外的函数。例如,如果合约为owner提供了一个接口来终止合约,那么整个代码可能会与其他任何状态变量一起被删除。
contract UnsafeContract3{
/* Define the contract owner*/
address owner;
/* This function sets the owner of the contract */
function initowner(address _owner) { owner = _owner; }
/* Function to destroy the contract */
function kill() { if (msg.sender == owner) suicide(owner); }
}
类似的攻击最近发生在Parity上,结果攻击者(或很有可能是一个好奇的新手,信息来源:https://blog.springrole.com/parity-multi-sig-wallets-funds-frozen-explained-768ac072763c?gi=c41592bf2d43)冻结了大约1.5亿美元。
反思一下
通过刚刚的展示,我们看到了智能合约中一些知名的漏洞,我们应该问自己:这些漏洞与传统漏洞有何不同?该如何避免?
我们所看到的大多数漏洞并不一定是智能合约的专有现象,其实在传统软件开发中也很常见。因此,人们可能很好地运用一些以前积累的知识来处理这些漏洞,例如:
在实例1中对于UnsafeContract1,问题是shares在msg.sender.call.value()后得到更新。防止攻击的一种可能的解决方案是,遵循一种所谓的“检查-效果-通信”(check-effect-communicate)设计模式。这种设计模式要求合约首先获得需要发送的金额,并执行任何其它本地检查,然后更新状态变量并最终与外部世界通信。下面的合约是对UnsafeContract1的修复。
contract FixedContract1{
// Mapping of address and amount
mapping(address => uint) shares;
// Withdraw a share
function withdraw() public {
uint share = shares[msg.sender];
shares[msg.sender] = 0;
msg.sender.transfer(share);
}
}
现在,即使恶意合约尝试回调,它也无法重复提取金额,因为映射shares在第一次调用后已经更新。
但是,当开发者不遵循这个安全指导时会发生什么?我们是否可以设计一种新的智能合约语言,在语言层面给开发人员强加这一指导,从而避免开发者再犯同样的错误?该语言应该清晰地在计算(数学和状态变化)和与外界的交流之间划清界限。换句话说,语言结构应该解决区块链交互(即发送和接收资金、信息)中合约的特定效应(例如函数)的问题,从而为潜在的合约构成和不变量提供一个清晰的推理机制。
如果没有适当的界限,计算和通信的复杂交织可能会导致合约有漏洞,从而被恶意方利用。这种界限最好在语言层面进行定义。
对于UnsafeContract2和UnsafeContract3,合约应同样将可变和不可变状态变量分开。一种新的智能合约语言可以为用户在可变和不可变变量之间提供清楚的区分。不可变变量只能在创建合约时实例化,并且在之后任何时候都不可修改。关键变量应只被认为是不可变的。
结论
我们今天看到的智能合约实例并不算复杂,但是在实例1的UnsafeContract1中两条指令排序的简单错误就可能会导致巨大的财务损失。此外,由于智能合约在拜占庭环境中运行,且在开发阶段难以预测可能发生的攻击,因此开发者难以推断智能合约的正确性和安全性。
考虑到这一点,我们就需重新要设计一个更好、更安全的智能合约语言。它可以减轻开发人员推理合约的任务,并且具有原则性和结构性,可以直接在语言层面消除某些已知漏洞,使合约更加安全。
那么,我们所说的智能合约的“安全性”具体指什么?我们希望保证哪些安全性?为了确保这些属性我们应如何设计语言?如何在语言中划清通信和计算方面的界限?这些问题将是下一篇文章的主题。敬请关注!
我们很高兴地邀请您加入我们的社区,与技术专家、金融业者和加密数字货币爱好者们一同探讨!您可以通过以下方式关注我们的进展:
微博:https://weibo.com/zilliqa
微信公众号:ZilliqaCN
Zilliqa中文社区联盟: http://www.zilliqa.com.cn
关注我们的推特:https://twitter.com/Zilliqa
通过邮箱订阅我们的新闻:http://zilliqa.us16.list-manage.com/subscribe?u=52acaef93d75cf69065e355ff&id=11f0b30bdd
关注我们的博客:https://blog.zilliqa.com/
Reddit:https://www.reddit.com/r/zilliqa
Slack:https://invite.zilliqa.com/
Gitter:https://gitter.im/Zilliqa/
电报群:https://t.me/zilliqachat