EOS智能合约安全终极指南

EOS智能合约安全终极指南。当世界上最大的ICO,EOS于2018年6月推出时,加密社区变得持怀疑态度,并且由于软件错误而被冻结了2天。但快进4个月,EOS今天占了以太网今天所做交易的两倍以上。通过免费和更快速交易的承诺,EOS最顶级的Dapp拥有大约13,000个每日活跃用户,而以太网的最顶级Dapp只有2,000个。

一些常见的智能合约漏洞几乎适用于所有平台。与以太坊一样,在EOS上编写的智能合约需要在主网上上线之前进行审核。合约中的致命错误可以在合约没有经过足够的测试时被利用。在本指南中,我们将帮助你避免在EOS上制作下一个杀手dApp的过程中常见的陷阱。

在阅读本指南之前,了解有关EOS开发的一些先决条件信息非常重要,这些信息在你阅读本指南时会很方便。了解C++是必须的。开始智能合约开发的最佳位置是EOSIO自己的文档。

ABI调用处理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
extern "C" {
void apply(uint64_t receiver, uint64_t code, uint64_t action) {
class_name thiscontract(receiver);

if ((code == N(eosio.token)) && (action == N(transfer))) {
execute_action(&thiscontract, &class_name::transfer);
return;
}

if (code != receiver) return;

switch (action) { EOSIO_API(class_name, (action_1)(action_n))};
eosio_exit(0);
}
}

上面是修改后的ABI调用程序的示例代码。如下所示的更简单的ABI调用程序用于简化合约的操作处理。

1
EOSIO_ABI( class_name, (action_1)(action_n) );

ABI调用程序/交易处理程序允许合约收听传入的eosio.token交易时间,以及与智能合约的正常交互。为了避免异常和非法调用,绑定每个键操作和代码以满足要求是很重要的。

一个例子是由于他们的ABI转发源代码中的错误而发生在dApp EOSBet Casino上的黑客攻击 。

1
2
3
4
5
6
if( code == self || code == N(eosio.token) ) {
TYPE thiscontract( self );
switch( action ) {
EOSIO_API( TYPE, MEMBERS )
}
}

上面检查ABI转发源代码的apply动作处理程序允许攻击者完全绕过eosio.token::transfer()函数,并在放置之前直接调用contract::transfer()函数而不将EOS转移到合约中。打赌。对于损失,他没有得到任何报酬,但一无所获。然而,对于胜利,他从合约中支付了真正的EOS。

他们通过在传入操作请求合约之前添加eosio.token合约转移操作检查来修复上述错误。

1
2
3
4
5
6
7
8
9
if( code == self || code == N(eosio.token) ) {
if( action == N(transfer) ){
eosio_assert( code == N(eosio.token), "Must transfer EOS");
}
TYPE thiscontract( self );
switch( action ) {
EOSIO_API( TYPE, MEMBERS )
}
}

使用语句require_auth(account)非常重要;进入只需要授权帐户才能执行的操作。require_auth(_self);用于仅授权合约的所有者签署交易。

操作中的授权

1
2
3
4
5
6
7
8
9
void token::transfer( account_name from, account_name to, asset quantity)
{
auto sym = quantity.symbol.name();
require_recipient( from );
require_recipient( to );
auto payer = has_auth( to ) ? to : from;
sub_balance( from, quantity );
add_balance( to, quantity, payer );
}

上面的示例代码允许任何人调用该操作。要解决它,请使用require_auth(from),声明授权付款人调用该行动​​。

尽量避免修改eosio.token合约

最近一位白帽黑客因其eosio.token合约中的方法调用问题而设法获得了10亿美元的dapp代币。Dapp Se7ens(现在处于非活动状态)在eosio.token合约中声明了一种新方法,用于将其代币空投到用户帐户中。合约没有反映eosio.token合约的问题或转移操作的变化,因此资金神奇地出现在用户的帐户上。其次,他们忘记在转移之前验证方法中的金额,这允许黑客在此过程中索取10亿个代币。

除了更改最大供应和代币符号之外,建议避免为自定义函数修改它,因为eosio.token合约中的错误可能是致命的。为了便于安全地进行空投,将空投代币转移到一个单独的帐户并从那里分发。

修改多索引表属性

EOS当前将数据存储在共享内存数据库中,以便跨操作共享。

1
2
3
4
5
6
7
8
9
10
11
struct [[eosio::table]] person {
account_name key;
std::string first_name;
std::string last_name;
std::string street;
std::string city;
std::string state;

uint64_t primary_key() const { return key; }
};
typedef eosio::multi_index<N(people), person> address_index;

上面的示例代码创建了一个名为peoplemulti_index表 ,该表基于使用struct person的该表的单行的数据结构。部署后,EOS目前不允许修改表属性。eosio_assert_message断言失败将是将被抛出的错误。因此,在部署表之前需要完全考虑属性。否则,需要创建具有不同名称的新表,并且在从旧表迁移到新表时需要特别小心。如果不这样做可能会导致数据丢失。

数值外溢检查

在进行算术运算时,如果没有足够负责地检查边界条件,则值可能会溢出,从而导致用户资产丢失。

1
2
3
4
5
6
void transfer(symbol_name symbol, account_name from, account_names to, uint64_t balance) {
require_auth(from);
account fromaccount;
eosio_assert(is_balance_within_range(balance), "invalid balance");
eosio_assert(balance > 0, "must transfer positive balance"); uint64_t amount = balance * 4; //Multiplication overflow
}

在上面的示例代码中,使用uint64_t表示用户余额可能会在值乘以时导致溢出。因此,尽量避免使用uint64_t来表示余额并对其执行算术运算。使用eosiolib中定义 的asset结构进行操作,而不是处理溢出条件的精确余额。

考虑合约中的假设

在执行合约时会有假设需要断言。如果断言失败,使用eosio_assert将事先处理条件并停止执行特定操作。举个例子:

1
2
3
4
void assert_roll_under(const uint8_t& roll_under) {
eosio_assert(roll_under >= 2 && roll_under <= 96,
"roll under overflow, must be greater than 2 and less than 96");
}

上面的断言语句假设roll_under整数大于2且小于96.但如果不是,则抛出上述消息并停止执行。没有发现像上面这样的局部问题可能会成为规则制定的全局灾难。

生成正确随机数

如果没有准确完成,在EOS区块链上生成正确的随机数仍然存在风险。如果没有正确地做到这一点,将导致对手预测结果,在整个过程中对整个系统进行游戏。像Oracalize.it这样的服务可以提供来自外部源的随机数,但它们很昂贵并且可能出现单点故障。人们过去曾使用Blockchain的上下文变量(块编号,块时间戳等)来生成以太坊智能合约中的随机数,这只是在游戏中使用。为了正确地进行生成,程序必须提供一种单一方无法单独控制的组合随机性。目前最好的方法之一是Dan Larimar自己生成随机数时建议的方法。

1
2
3
4
5
6
7
8
9
10
11
12
string sha256_to_hex(const checksum256& sha256) {
return to_hex((char*)sha256.hash, sizeof(sha256.hash));
}

string sha1_to_hex(const checksum160& sha1) {
return to_hex((char*)sha1.hash, sizeof(sha1.hash));
}
template <class T>
Inline void hash_combine(std::size_t& seed, const T& v) {
std::hash<T> hasher;
seed ^= hasher(v) + 0x9e3779b9 + (seed << 6) + (seed >> 2);
}

上面的示例代码给出了1到100之间的优化随机数生成.seed1是种子,seed2是上面的用户种子。作为参考,DappubEOSBetCasino开放了他们的完整合约,随机数发生器实现了玩家和开发商之间的公平骰子游戏。

1
2
3
4
5
6
uint8_t compute_random_roll(const checksum256& seed1, const checksum160& seed2) {
size_t hash = 0;
hash_combine(hash, sha256_to_hex(seed1));
hash_combine(hash, sha1_to_hex(seed2));
return hash % 100 + 1;
}

EOSBet最近再次遭到65,000EOS的攻击,当一个对手欺骗他们的eosio.token合约时,只要他在自己的钱包之间进行交易,就将EOS送到他的钱包。eosio.token合约代码 通知EOS代币的发送者和接收者有传入代币。为了模仿这种行为并促进黑客攻击,对手创建了两个帐户,让我们假设A和B。A与智能合约的操作有声明require_recipient(N(eosbetdice11))。当A通过操作调用促进了从A到B的交易时,它通知合约中的转移功能,就好像该呼叫来自eosio.token合约一样。由于EOS没有真正转移到合约中,每当黑客输掉赌注时,他什么都没有丢失,但是在赢得赌注时他获得了奖励。因此,仅检查合约名称和操作名称是不够的。

检查合约中的通知

为了缓解这个问题,该函数应检查合约是否确实是代币的接收者。

1
eosio_assert(transfer_data.from == _self || transfer_data.to == _self, "Must be incoming or outgoing transfer");

在EOS上制定智能合约时应遵循的最佳做法是什么?

错误是任何软件不可避免的一部分。它的后果在去中心化的环境中被放大,特别是如果它涉及价值交易。除了上面讨论的EOS特定保护措施之外,以下是新智能合约开发人员应该记住的一些一般预防措施和最佳实践:

  • 在主网上发布之前,始终独立于第三方智能合约审计公司审计合约。
  • 在发布到testnet之前,进行必要的Caveman调试(当前调试合约的唯一方法)。EOSIO文档有一个很好的指南。
  • 设置提款限额转移率,避免主网启动初期出现过多损失。有白帽黑客负责任披露的bug赏金计划。
  • 当检测到错误时,有一个killswitch来冻结合约。

为了实现它,我们在multi_index表中保留一个标志。我们使用只能由合约所有者调用的操作来设置标志。然后我们检查每个公共行动是否将标志设置为冻结。下面给出了该函数的示例实现。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
struct st_frozen {
uint64_t frozen;
};

typedef singleton<N(freeze), st_frozen> tb_frozen;
tb_frozen _frozen;

uint64_t getFreezeFlag() {
st_frozen frozen_st{.frozen = 0};
return _frozen.get_or_create(_self, frozen_st);
}

void setFreezeFlag(const uint64_t& pFrozen) {
st_frozen frozen_st = getFreezeFlag();
frozen_st.frozen = pFrozen;
_frozen.set(frozen_st, _self);
}

// public Action
void freeze() {
require_auth(_self);
setFreezeFlag(1);
}

// public Action
void unfreeze() {
require_auth(_self);
setFreezeFlag(0);
}
// any public action
void action(...) {
eosio_assert(getFreezeFlag().frozen == 1, "Contract is frozen!");
...
}

随时了解库中的安全性增强或平台上的漏洞披露。必要时立即更新库。至少开源合约代码,以便在项目中保持公平性,独立开发人员可以更快地发现错误。

EOS智能合约安全:结论

自EOS推出仅仅5个月,它已经超出了预期。它所取得的DPOS,可变智能合约,21个采矿节点等,当然受到权力下放极端主义者的严厉批评。然而,考虑到平台今天提供的可扩展性,它并没有阻止基于以太坊的dApp转向EOS。无论是EOS还是以太坊赢得战争还有待确定,但EOS肯定赢得了这场战斗。它将保持不变,直到以太坊设法达到运行“世界计算机”所需的世界所需的可扩展性。

======================================================================

分享一些以太坊、EOS、比特币等区块链相关的交互式在线编程实战教程:

  • EOS教程,本课程帮助你快速入门EOS区块链去中心化应用的开发,内容涵盖EOS工具链、账户与钱包、发行代币、智能合约开发与部署、使用代码与智能合约交互等核心知识点,最后综合运用各知识点完成一个便签DApp的开发。
  • java以太坊开发教程,主要是针对java和android程序员进行区块链以太坊开发的web3j详解。
  • python以太坊,主要是针对python工程师使用web3.py进行区块链以太坊开发的详解。
  • php以太坊,主要是介绍使用php进行智能合约开发交互,进行账号创建、交易、转账、代币开发以及过滤器和交易等内容。
  • 以太坊入门教程,主要介绍智能合约与dapp应用开发,适合入门。
  • 以太坊开发进阶教程,主要是介绍使用node.js、mongodb、区块链、ipfs实现去中心化电商DApp实战,适合进阶。
  • C#以太坊,主要讲解如何使用C#开发基于.Net的以太坊应用,包括账户管理、状态与交易、智能合约开发与交互、过滤器和交易等。
  • java比特币开发教程,本课程面向初学者,内容即涵盖比特币的核心概念,例如区块链存储、去中心化共识机制、密钥与脚本、交易与UTXO等,同时也详细讲解如何在Java代码中集成比特币支持功能,例如创建地址、管理钱包、构造裸交易等,是Java工程师不可多得的比特币开发学习课程。
  • php比特币开发教程,本课程面向初学者,内容即涵盖比特币的核心概念,例如区块链存储、去中心化共识机制、密钥与脚本、交易与UTXO等,同时也详细讲解如何在Php代码中集成比特币支持功能,例如创建地址、管理钱包、构造裸交易等,是Php工程师不可多得的比特币开发学习课程。
  • tendermint区块链开发详解,本课程适合希望使用tendermint进行区块链开发的工程师,课程内容即包括tendermint应用开发模型中的核心概念,例如ABCI接口、默克尔树、多版本状态库等,也包括代币发行等丰富的实操代码,是go语言工程师快速入门区块链开发的最佳选择。

汇智网原创翻译,转载请标明出处。这里是原文EOS智能合约安全终极指南