1. 链一财经首页
  2. 资讯

TRON智能合约安全小课堂 | 第七期:盘点危险的 delegatecall

欢迎来到

TRON智能合约安全小课堂

波场面向社区

征集DApp 的开源代码

结合其合约源码,以实战的方式

讲解波场智能合约开发时,需要注意的一些安全细节

 

本期小课堂重点讲一讲危险的 delegatecall。同时, 欢迎大家关注 @TRON 官方 Twitter,踊跃投稿合约代码。

 

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

 

图1 RouletteV5的合约源码

RouletteV4合约的代码很简短,核心代码如下。

1.contract RouletteV5 {

2.    address public owner = msg.sender;

3.    uint256 public lastPlayed;

4.    uint256 public secretNumber;

5.    uint256 public lastBet;

6.    uint256 public betPrice = 1000;

7.    struct Game {

8.        uint256 number;

9.        uint256 secret;

10.        address player;

11.    }

12.    address public logger;   

13.    Game[] public gamesPlayed;

14.

15.    constructor() public {

16.        lastPlayed = now;

17.        shuffle();

18.}

19.

20.    function play(uint256 number_) payable public {

21.        require(msg.value >= betPrice && number_ <= 10);

22.        lastPlayed = now;

23.        

24.        logger.delegatecall(bytes4(keccak256(“logPlay(uint256,uint256)”)), secretNumber, number_);

25.        

26.        gamesPlayed.push(Game(number_, secretNumber, msg.sender));

27.

28.        if (number_ == secretNumber) { // win!

29.            msg.sender.transfer(address(this).balance);

30.        }

31.        lastBet = msg.value;

32.        

33.        shuffle();

34.    }

35.

36.    function kill() public {

37.        logger.delegatecall(bytes4(keccak256(“logKill()”)));

38.        require(owner == msg.sender && now > lastPlayed + 1 days);

39.        selfdestruct(msg.sender);

40.    }

这次仍然延续了之前使用的轮盘赌合约,合约初始化的时候, 设置了一个 1-20 的随机数:secretNumber,玩家通过 play()竞猜这个数字,并更新secretNumber。为了方便演示,合约的secretNumber设置为 public,同时提供了多个查询函数。

但是,通过第24行的 delegatecall,玩家永远无法通过play()中奖。怎么做到的?

我们都知道,delegatecall,是把被调用合约的代码放在当前合约的数据空间里执行,下面是被调用合约的核心代码,这也是很多开发者觉得 delegatecall 非常危险的原因。下面是  被调用的Logger的核心代码.

1.contract Logger {

2.    

3.    uint256 public time = now;

4.    uint256 public lastPlayed;

5.    address public player;

6.    

7.    

8.    function logPlay(uint256 secretNumber, uint256 number) payable public {

9.        player = msg.sender;

10.    }

11.    

12.    function logKill() public {

13.        lastPlayed = 0;

14.    }

15.}

可以看到, logger.delegatecall(bytes4(keccak256(“logPlay(uint256,uint256)”)), secretNumber, number_)调用的是 Logger 合约的 logPlay 方法. 在 Logger 的第9行, 修改了 Logger 合约的 player 的值. 那么它对外层调用合约有什么影响, 导致RouletteV5的 play() 永远无法猜中secretNumber?

这需要说一说 Solidity 的成员变量的存储方式。

静态大小的变量(除映射mapping 和动态数组之外的所有类型)都从位置 0 开始连续放置在存储(storage) 中。另外,如果可能的话,存储需求少于 32 字节的多个变量会被打包到一个 存储插槽。

换句说,对于 solidity 来说,变量的存储位置和变量的名称名没有任何关系,只和它在变量列表中的位置有关。

所以 Logger 的 player 变量,对应RouletteV5中的第3个变量secretNumber。而且 uint256和 address 的存储长度都是 32 bytes。所以 在 RouletteV5通过 delegatecall调用Logger 的第9行时,会覆盖 RouletteV5的secretNumber为一个 uint256(msg.sender), 它的值一般来说,非常非常大。那么也就无法猜中了。

RouletteV5的37行也是以类似的方式,通过 Logger 的13行,将 lastPlayed 修改为0,所以通过了require(owner == msg.sender && now > lastPlayed + 1 days)。

最后,你可能注意到了 Logger 的两个函数,一个有payable标识,一个没有。那么:

1.有没有 payable标识,会对相应的 bytecode 有啥区别呢?

2.又会导致相应的 delegatecall 有啥影响呢?

3.为啥有这种影响呢?

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

 

本次小课堂先讲到这里。感谢TRON-Eye 提供合约验证工具,有关他们的更多信息可直接访问https://troneye.com。我们将继续征集后续课堂的范例源码, 欢迎关注官方 Twitter 踊跃投稿。

根据国家《关于防范代币发行融资风险的公告》,大家应警惕代币发行融资与交易的风险隐患。

本文来自LIANYI转载,不代表链一财经立场,转载请联系原作者。

发表评论

登录后才能评论