1. Hi区块链首页
  2. 资讯
  3. 技术指南

TRON智能合约安全小课堂 | 第十期:关于Create2的问题

本期小课堂将讨论create2的问题。

本期小课堂将讨论create2的问题。同时,欢迎大家关注 @tron官方 twitter,踊跃投稿合约代码。

下面是从https://troneye.com (以下简称 TRON-Eye)查询到的合约代码。TRON-Eye 是来自社区的波场合约验证平台,之前的小课堂已经对 TRON-Eye 验证平台进行了详细的介绍。本次的合约是ShipperConfirmed(地址: https://troneye.com/reveal?address=TWxpJVxSJ2VFNZQaiffLXcuAQbx4yPMarA)。 

640?wx_fmt=png&tp=webp&wxfrom=5&wx_lazy=1&wx_co=1

图1 ShipperConfirmed的合约源码

先来聊一下波场中智能合约地址生成方式。目前有两种方式:

1. 使用deployContract(一种交易类型)部署合约的时候会生成地址,公式为:

其中是波场地址的固定前缀0x41,是当前交易hash,即为合约部署者的地址。

2. 在solidity代码中使用create指令创建合约的时候,地址生成公式为:

其中是波场地址的固定前缀0x41,是当前交易hash,为当前合约(create指令所在的合约)已经进行的外部调用的次数。

在波场编译器0.5.4版本中新加了create2指令,在java-tron Odyssey-v3.6.0 也支持了create2指令。这样在波场中又多了一种生成合约地址的方式,公式为: 

其中是波场地址的固定前缀0x41,是当前合约(create2指令所在的合约)的msg.sender,salt是一个可以自定义的盐值,是新部署合约的原始code,和deployContract中使用的code相同,包含constructor和初始化参数在内。

需要注意的一点是, 和最后存储在智能合约中的bytecode不同,存储的bytecode不包括constructor和初始化参数。

使用create2指令也非常简单,示例为:

1 pragma solidity ^0.5.0;

2 contract Factory {

3     event Deployed(address addr, uint256 salt);

4     function deploy(bytes memory code, uint256 salt) public {

5         address addr;

6         assembly {

7             addr := create2(0, add(code, 0x20), mload(code), salt)

8             if iszero(extcodesize(addr)) {

9                 revert(0, 0)

10            }

11       }

12       emit Deployed(addr, salt);

13    }

14 }

为什么需要create2这条新指令呢?

create2提供了一个事先确定合约地址的方法,而create指令无法做到,因为txHash对应的交易信息中包含时间戳,也就是说txHash会随着时间的变化而变化,当超过一定时间之后,这个交易就会失效,不能上链。而create2指令确保,只要address、salt、init_code都相同,那么地址肯定是相同的,这个属性对于链下扩容至关重要。比如,链下计算的参与方,如果没有争端的话,就不需要一个解决争端的合约,当发生争端了,需要提交给tron判定谁对谁错的时候,才需要这个解决争端的合约,这个时候,create2就非常有用。参与方可以事先商量好了这个合约的地址,基于这个地址在链下做一系列的操作,当有争端了,再提交这个合约。这样就大大减少了无意义的合约的部署,让产块节点去处理更有用的交易,也节省了energy的消耗。

但是,这种在预定地址部署合约的方式也会带来一些安全隐患,特别是当使用create2指令创建的合约有selfdestruct的时候。主要安全隐患有三方面:

1. selfdestruct之后,所有的storage状态变量和余额等都会清0。那么重新使用create2指令部署合约的时候,相当于给合约添加了重置所有变量的功能,但这不是我们希望看到的结果,或者会影响我们对智能合约安全性的判断。我们上面说的ShipperConfirmed就是这样的例子。

2. init_code不等同于存储在链上智能合约中的bytecode,利用智能合约提供的全局变量(比如msg.value、blockhash等)或者外部变量(比如存储在其他合约中某个值),来决定使用不同的初始化参数,从而设置合约的不同状态。这样的话,虽然合约地址相同,但是在selfdestruct之后redeploy的时候,合约的状态就不知不觉的修改了。

3. 同第2点,init_code不等同于存储在链上智能合约中的bytecode,使用外部变量可以部署不同的bytecode。

存在安全隐患的根本原因在于,不考虑hash碰撞的前提下,在create2指令之前,一个合约的状态只有三种:1.不存在,2.存在,3.自毁。但是在create2指令后,一个合约的状态变成了四种:1.不存在,2.存在,3.自毁,

4.重新部署。如果用户没有对此保持警觉,还是按照之前的思路,那么很有可能被骗。下面开始以ShipperConfirmed进行第一种情况的讨论,源码为:

1   pragma solidity ^0.5.0;

2   contract ShipperConfirmed {

3       address owner;

4       address public shipper;

5       bool public isConfirmed = false;

6       event Confirmed();

7       constructor() public {

8           owner = msg.sender;

9       }

10      modifier onlyOwner {

11          require(msg.sender == owner);

12          _;

13      }

14      function setShipper(address newShipper) external {

15          if (shipper == address(0)) {

16              shipper = newShipper;

17          }

18      }

19      function confirmShip() external {

20          if (msg.sender == shipper) {

21              isConfirmed = true;

22              emit Confirmed();

23          }

24      }

25      function shutdown() external onlyOwner {

26          selfdestruct(msg.sender);

27      }

28  }

这是一个示例,当货物寄出,并且是用户所相信的人成为shipper之后,用户就完全相信接下来的状态的改变,假设用户通过监听日志,发现已经被设置为寄出状态时,就发送报酬给商家。用户是聪明的,看到这个合约,发现,一但shipper被设置后,不能被再次设置,所以当用户看到已经是自己相信的寄出者之后,就完全相信。在create2指令之前,这是没有问题的,但是有了create2,开发者可以selfdestruct之后redeploy,然后直接设置自己为shipper,修改寄出的状态。

上面的例子仅仅是利用selfdestruct-redeploy修改合约状态的一个简单的情况。

稍微延展一下:对于第二种情况和第三种情况,大家能否参考文中的例子,提供源码呢?

欢迎关注twitter,你来告诉我答案。

本次小课堂先讲到这里。感谢TRON-Eye 提供合约验证工具。有关他们的更多信息可以直接访问他们的官网https://troneye.com。

我们继续征集后续课堂的范例源码, 非常欢迎关注我们的官方 twitter 踊跃投稿。

声明:登载此文出于传递更多信息之目的,观点仅代表作者本人,绝不代表Hi区块链赞同其观点或证实其描述。
提示:投资有风险,入市须谨慎。本资讯不作为投资理财建议。