前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >Ethereum 01 - 保证智能合约的安全可靠

Ethereum 01 - 保证智能合约的安全可靠

作者头像
Reck Zhang
发布2021-08-11 11:44:03
2970
发布2021-08-11 11:44:03
举报
文章被收录于专栏:Reck Zhang

保证智能合约的安全可靠

首先要注意, 即使智能合约代码中没有任何Bug, 编译器和运行平台本身也可能有Bug.

详见List of Know Bugs of Solidity.

常见的安全陷阱

私有状态

在Solidity中, 可以通过private定义私有变量或者私有函数. 但是开发者要意识到, 在区块链上智能合约的所有信息都是公开可见的, 即使是被private修饰的变量(事实上, private变量仅仅是不能被其他智能合约在执行时”直接”访问到而已). 只是因为每个全节点都可以拿到智能合约创建和调用的字节码, 他们都会将智能合约执行后的状态保存在本地以供验证, 而所有的变量都可以通过eth_getStorageAt()这样的API探测到.

随机

在智能合约中使用随机数是一件十分微妙的事情, 因为所谓的随机是由创建当前区块的”矿机”决定的, “矿机”虽然不能篡改执行结果, 但是有可能预先得知随机数(the casino with a public RNG seed), 所以自己做轮子写随机数生成器是很危险的.

重入

在计算机程序中, 重入(Re-Entrancy)是指一段程序在执行过程中被打断, 并且在上一次调用还未完全结束之前再次被重新调用的现象.

任何从合约A到合约B的转账过程中, 将控制权移交给合约B的行为都有可能造成合约B在转账完成之前再次调用合约A.

代码语言:javascript
复制
// bug contain
contract Fund {
    // mapping of ether shares of the contract
    mapping(address => uint) shares;
    // withdraw your share
    function withdraw() {
        if(this.balance < shares[msg.sender]) {
            throw;
        }
        if(msg.sender.call.value(shares[msg.sender])()) {
            shares[msg.sender] = 0;
        }
    }
}

在这段智能合约中, 有一个mapping类型的变量shares, 和一个withdraw函数. shares记录着每个用户在Fund中拥有的”股权”, 假定一份Fund的股权等价于一个以太币. 当用户想将在Fund中的”股权”换回以太币时, 可以通过调用withdraw()函数进行撤回操作.

在这段代码中, Fund先将以太币返还给用户, 再将shares里记录的相应”股权”清零. 当一个普通的用户账户调用执行退款时, 另一个智能合约同时来调用, 就会有严重的隐患.

由于Gas的限制的限制, 我们不需要担心死循环的问题. 但是以太币的转账总是会触发代码的执行, 如果接收方是一个智能合约, 即msg.send是一个智能合约, 那么他将能够在接收过程中再次调用withdraw()函数. 具体做法是, 接收方智能合约中自己定义一个匿名函数, 在这个匿名函数中再次调用withdraw()函数. 由于在执行msg.sender.call时, 接收方合约(msg.sender)的匿名函数是会自动执行的, 这将导致接收方合约的匿名函数和Fund合约的匿名函数之间循环调用, 使得Fund合约一直执行不到shares[msg.sender] = 0这句话, 而重复地执行msg.sender.call.value(shares[msg.sender])(). 这样一直执行下去, Fund被反复提款, 要么调用栈到达最大深度, 要么Fund合约中的余额不足, 才会使得程序的执行被终止.

为了避免重入问题, 可以像下面代码一样进行检测.

代码语言:javascript
复制
contract Fund {
    // mapping of ther shares of the contract
    mapping(address => uint) shares;
    // withdraw your share
    function withdraw() {
        uint share = shares[msg.sender];
        if(this.balance < share) {
            throw;
        }
        shares[msg.sender] = 0;
        msg.sender.transfer(share);
    }
}

要注意的是, 不仅是以太币转账会带来重入问题, 其他任何对其他合约的访问都会有一样的问题. 此外, 在使用多重组合的合约时, 被调用的合约也可能修改调用合约所依赖的另一个合约的状态.

Gas限制和循环

在以太坊智能合约中, 每一步操作是要求用户以Gas的形式付出相应的代价. 对于非固定次数的循环(如依赖于某一个存储的值)的使用, 一定要格外的注意.

由于每个区块的Gas限制(Gas Limit), Transaction最多能消耗的Gas是有限的. 固定次数的循环可以准确的计算出消耗的Gas, 从而可以避免执行智能合约消耗的Gas超出限制.

因此设计者必须考虑到这一点, 可以通过限定最大循环次数方式, 来避免发生对智能合约的某次调用不能在Gas限制之内执行完毕的情况.

tx.origin和msg.sender

Solidity提供两两个方式来获取调用者的身份: tx.origin和msg.sender. 不过这两者有明显的区别. tx.orgin是用来获取发起Transaction的账户地址, 而msg.sender只能获取上一级调用者的地址. 比如智能合约A调用了智能合约B, 有一个用户P给智能合约A发Transaction, 调用A调用了B. 那么对于智能合约A来说, msg.sender和tx.origin都是P的地址, 而对于智能合约B来说, msg.sender是A的地址, tx.origin是P的地址.

按照Solidity官方的建议, 不推荐使用tx.orgin进行权限控制.

代码语言:javascript
复制
// bug contain
contract TxUserWaller {
    address owner;

    function TxUserWallet() {
        owner = msg.sender;
    }
    function transferTo(address dest, uint amount) {
        require(tx.origin == owner);
        dest.transfer(amount);
    }
}

黑客可以定义一个这样的智能合约:

代码语言:javascript
复制
interface TxUserWallet {
    function transferTo(address dest, uint amount);
}

contract TxAttackWallet {
    address hacker;

    function TxAttackWallet() {
        hacker = msg.sender;
    }

    function() {
        TxUserWallet(msg.sender).transferTo(hacker, msg.sender.balance);
    }
}

在TxAttackWallet中定义了一个匿名函数, 在这个函数中会调用TxUserWallet的transferTo函数. 黑客会欺骗他人给TxAttackWallet这个合约地址转钱, 甚至只是让他发生一个交易, 这都会触发TxAttackWallet的匿名函数. 通过这个匿名函数判断tx.origin确实是TxUserWallet合约的owner, 虽然owner没有直接调用transferTo函数, 但是owner在TxUserWallet中的钱都转到了hack账户.

因此msg.sender和tx.origin的使用应该结合具体场景, 开发者应该在充分理解二者的区别的基础上考虑合约的安全性.

其他细节

溢出

在Solidity中, uint是256位, 最大值为256^2 - 1, 最小是0. 那么当整形算数结果超过这个范围时, 会出现上溢或者下溢, 这是计算结果将于实际期望结果出现偏差. 因此在做四则运算的时候, 推荐使用SafeMath.

var

代码语言:javascript
复制
for(var i = 0; i < length; i++) {}

这里i的编译器设置为uint8, 因为uint8是能表示9值的最小类型, i最大只能为255, 那么如果length大于255, for将不会停止, 直到Gas耗尽.

智能合约开发建议

  1. 使用Checks-Effects-Interactions模式
    1. Check 在执行之前, 先进行权限及安全性检查. 检查的内容包括: 判断function的调用者身份, 判断是否有相关操作权限, 调用的参数是否符合要求, 调用function的Transaction是否附上了指定的以太币数量等.
    2. Effect 当必要的检查都通过了之后, 再对当前合同中的状态变量进行更改.
    3. Interactions 在状态变量的更改生效之后, 再进行和其他合约的交互.
  2. 充分的容错机制
    1. 使用Fail-Safe模式 所谓Fail-Safe, 就是在智能合约出现异常情况下, 能尽可能保障合约中数据的安全. 首先, 开发者需要在智能合约中添加一个自检查的函数, 在这个函数中对合约的状态进行检查, 特别是和数字资产相关的内容一定要格外注意. 一旦自检查函数执行出现异常, 那么要能自动的触发Fail-Safe模式, 这是可以将交易相关的函数禁用, 只允许指定合约的创始人或一个可信的第三方控制
    2. 限制合约中数字资产的数量 最好不要在智能合约中存储大量的数字资产, 毕竟以太坊社区本身还在不断发展, 很多新的特性被不断的引入, 很难保证每次升级都是稳定的.
    3. 让代码轻巧且模式化 为了保证代码的安全性, 清晰的代码结构, 易于理解的代码逻辑都是很必要的. 尽可能不要在一个函数里写太复杂的逻辑, 可以将工具性的逻辑抽离出来, 将一个复杂的函数拆成多函数. 另外和其他软件开发类似, 开发者最好有一份清晰详细的文档来介绍合约内容, 以及各个函数的功能和逻辑.
    4. 充分的测试 在以太坊主网络上正式发布智能合约前, 一定要做好充分的测试, 任何的漏洞都有可能让你损失惨重. 也可以用下面的工具进行检查:
      • Oyente: 一个Python语言编写的工具, 判断代码中有没有常见的安全漏洞, 也会提示出可能有安全隐患的地方.
      • solidity-coverage: 一个Node.js编写的Solidity代码覆盖率测试工具, 需要结合测试网络一起使用.
      • Solgraph: 一个Node.js工具, 可以将一个智能合约作为输入, 输出一个DOT图文件, 能将智能合约的功能控制流程画成一个流程图, 也可以标注出潜在的安全漏洞.
本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2018-05-11,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 保证智能合约的安全可靠
    • 常见的安全陷阱
      • 智能合约开发建议
      相关产品与服务
      区块链
      云链聚未来,协同无边界。腾讯云区块链作为中国领先的区块链服务平台和技术提供商,致力于构建技术、数据、价值、产业互联互通的区块链基础设施,引领区块链底层技术及行业应用创新,助力传统产业转型升级,推动实体经济与数字经济深度融合。
      领券
      问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档