本文英文版原地址:http://solidity-cn.readthedoc...
因为本人英语能力有限,使用谷歌翻译,本篇好多地方不通顺。有能力的可以直接看英文版本。

虽然构建按预期工作的软件通常非常容易,但要检查人们以不能预料到的方式使用它,要困难得多。

在Solidity中,这更加重要,因为您可以使用智能合约来处理令牌(tokens)或可能更有价值的东西。此外,智能合约的每一次执行都在公开场合进行,除此之外,源代码通常是可用的。

当然,你需要考虑有多大的风险:你可以将智能合约与对公众开放的Web服务(以及对恶意行为者)以及甚至开放源代码进行比较。如果您只将该购物清单存储在该Web服务上,则可能不必太在意,但如果您使用该Web服务管理您的银行账户,则应该更加小心。

本节将列出一些陷阱和一般安全建议,但当然可能永远不会完整。另外,请记住,即使您的智能合约代码没有缺陷,编译器或平台本身也可能有错误。可以在已知错误列表中找到编译器的一些公开已知安全相关错误列表,这些错误也是机器可读的。请注意,有一个错误赏金程序涵盖了Solidity编译器的代码生成器。

与往常一样,使用开源文档,请帮助我们扩展本节(特别是,一些示例不会受到伤害)!

陷阱

私人信息和随机性
您在智能合约中使用的所有内容都是公开可见的,即使是标记为private的本地变量和状态变量。

如果你不希望矿工能够作弊,在智能合同中使用随机数字是非常严峻的一件事。

重入(Re-Entrancy)

合同(A)与另一合同(B)的任何互动以及乙方的任何转让均将控制移交给该合同(B)。 这使得在这个交互完成之前B可以回调A. 举一个例子,下面的代码包含一个bug(它只是一个片段而不是一个完整的合同):

pragma solidity ^0.4.0;

//这个函数包含一个bug---不要使用

contract Fund {
    /// 合约的映射。
    mapping(address => uint) shares;
    /// 撤回你的份额。
    function withdraw() public {
        if (msg.sender.send(shares[msg.sender]))
            shares[msg.sender] = 0;
    }
}

这里的问题不是太严重,因为作为send的一部分,gas是有限的,但它仍然暴露出一个弱点:以太(Ether)转移总是包含代码执行,所以接收方可能是一个合约,可以调回撤回。 这将让它得到多次退款,并基本上检索合同中的所有以太网。 特别是,以下合同将允许攻击者多次withdraw,因为它使用默认转发所有剩余gascall

pragma solidity ^0.4.0;

// 这个函数包含一个bug---不要使用
contract Fund {
    mapping(address => uint) shares;
    function withdraw() public {
        if (msg.sender.call.value(shares[msg.sender])())
            shares[msg.sender] = 0;
    }
}

为了避免重新入侵(Re-Entrancy),您可以使用Checks-Effects-Interactions模式,如下所述:

pragma solidity ^0.4.11;

contract Fund {
    mapping(address => uint) shares;
    function withdraw() public {
        var share = shares[msg.sender];
        shares[msg.sender] = 0;
        msg.sender.transfer(share);
    }
}

请注意,重入不仅影响以太转移,还影响其他合同上的任何功能调用的。 此外,您还必须考虑多合同情况。 被called的合同可以修改您依赖的另一份合同的状态。

Gas限制和循环
没有固定迭代次数的循环(例如取决于存储值的循环)必须小心使用:由于区块中gas限制,交易只能消耗一定量的gas。 无论是明确的还是仅仅由于正常的操作,循环中的迭代次数可能会超出区块中gas限制,这会导致整个合同在某个点停滞。 这可能不适用于仅用于从区块链读取数据的constant函数。 尽管如此,这些功能可能会被其他合同作为链上操作的一部分进行调用,并将其拖延。 请在合同文件中明确说明这些情况。

发送和接收Ether

  • 合同和“外部账户”都不能阻止有人送他们Ether。 合同可以作出反应并拒绝定期转移,但有些方法可以在不创建消息呼叫的情况下移动Ether。 一种方法是简单地"mine to"合同地址和第二种方式使用selfdestruct(x)
  • 如果合同收到Ether(没有调用函数),则执行回退函数。 如果它没有后备功能,Ether将被拒绝(通过抛出异常)。 在执行回退功能时,合同只能依靠当时可用的“ gas津贴”(2300 gas)。 这笔津贴不能以任何方式访问存储。 为确保您的合同能够以此方式接收Ether,请检查故障预置功能的gas请求(例如,在Remix的“详细信息”部分中)。
  • 有一种方法可以使用addr.call.value(x)()将更多gas转发给接收合同。 这与addr.transfer(x)基本相同,只是它转发了所有剩余的gas并打开了接收方执行更昂贵的操作的能力(并且它只返回失败代码并且不会自动传播错误)。 这可能包括回拨发送合约或您可能没有想到的其他状态更改。 因此它为诚实用户提供了极大的灵活性,同时也为恶意行为者提供了很大的灵活性
  • 如果你想使用address.transfer发送Ether,有一些细节需要注意:

    1.如果收件人是合同,它将导致其执行回退功能,从而可以回拨发送合同。
    2.发送Ether可能会因呼叫深度超过1024而失败。由于caller完全控制呼叫深度,因此可能会强制传送失败; 考虑这种可能性或使用发送,并确保始终检查其返回值。 更好的是,用收款人可以取消Ether的模式写下你的合同。
    3.发送Ether也可能失败,因为收货合同的执行需要的gas超过了分配的数量(明确地通过使用要求,断言,还原,抛出或因为操作太昂贵) - 它“耗尽gas”(OOG)。 如果您使用转账或发送返款金额支票,这可能为收件人提供阻止发送合同中进度的手段。 同样,这里的最佳做法是使用“撤回”模式而不是“发送”模式。

Callstack深度
外部函数调用可能会随时失败,因为它们超过了1024的最大调用堆栈。在这种情况下,Solidity会引发异常。 恶意行为者在与你的合同进行交互之前可能会强制调用堆栈的high value。

请注意,如果调用堆栈已耗尽,则.send()不会引发异常,但在此情况下返回false。 低等级函数.call().callcode().delegatecall()的行为方式相同。

tx.origin
切勿使用tx.origin进行授权。 假设你有这样的钱包合约:

pragma solidity ^0.4.11;

//这个函数包含一个bug---不要使用
contract TxUserWallet {
    address owner;

    function TxUserWallet() public {
        owner = msg.sender;
    }

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

现在有人欺骗你将ether发送到这个攻击钱包的地址:

pragma solidity ^0.4.11;

interface TxUserWallet {
    function transferTo(address dest, uint amount) public;
}

contract TxAttackWallet {
    address owner;

    function TxAttackWallet() public {
        owner = msg.sender;
    }

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

如果您的钱包已经检查了msg.sender的授权,它会得到攻击钱包的地址,而不是所有者地址。 但通过检查tx.origin,它会得到启动交易的原始地址,该地址仍是所有者地址。 攻击钱包立即消耗您的所有资金。

备注

  • for(var i = 0; i <arrayName.length; i++){...}中,i的类型将是uint8,因为这是保存值0所需的最小类型。如果数组有255个元素以上,循环将不会终止。
  • 函数的constant关键字目前不由编译器强制执行。此外,它不是由EVM强制执行的,所以“声称”保持不变的合约功能可能仍会导致状态发生变化。
  • 不占用完整32字节的类型可能包含“脏高位”。 有一点非常重要,如果您访问msg.data(这构成了可塑性风险):您可以使用原始字节参数为0xff0000010x00000001调用函数f(uint8 x)。这两种方法都是与合同相关联的,而且它们看起来都像x相关的数字1。但是msg.data会有所不同,所以如果您使用keccak256(msg.data)做任何事情,您将得到不同的结果。

推荐做法
限制Ether的量。
限制可以存储在智能合约中的Ether(或其他tokens)数量。 如果您的源代码,编译器或平台有错误,这些资金可能会丢失。 如果你想限制你的损失,限制Ether的数量。

保持小型化和模块化
保持合同规模小,易于理解。 在其他合同或库中找出无关的功能。 关于源代码质量的一般建议当然适用:限制局部变量的数量,函数的长度等等。 记录你的功能,以便其他人可以看到你的意图是什么,以及它是否与代码不同。

使用检查 - 效果 - 互动(Checks-Effects-Interactions )模式
大多数函数将首先执行一些检查(谁调用函数,是范围内的参数,他们是否发送了足够多的Ether,人员是否具有tokens等)。 这些检查应该先完成。

作为第二步,如果所有检查都通过了,则应该对当前合同的状态变量产生影响。 与其他合同的交互应该是任何功能的最后一步。

早期合同延迟了一些效果,并等待外部函数调用以非错误状态返回。 由于上述重入问题,这通常是一个严重的错误。

请注意,对已知合同的调用也可能导致对未知合同的调用,所以最好始终应用此模式。

包含故障安全模式
在使系统完全分散化的同时将删除任何中介,这可能是一个好主意,特别是对于新代码,可能包含某种故障安全机制:

您可以在智能合约中添加一个函数,执行一些自我检查,如“有任何Ether泄露?”,“tokens的总和是否等于合同的余额?” 或类似的东西。 请记住,你不能使用太多的gas,所以通过脱链(off-chain)计算可能需要帮助。

如果自检失败,合同会自动切换到某种“故障安全”模式,例如,禁用大部分功能,将控制权移交给固定和受信任的第三方,或者仅将合同转换为简单的“ 把我的钱还给我“合同。

形式化验证
使用形式验证,可以执行自动化的数学证明,证明源代码符合特定的正式规范。 规范仍然是正式的(就像源代码一样),但通常要简单得多。

请注意,形式验证本身只能帮助你理解你所做的事情(规范)和你如何做(实际实现)之间的差异。 您仍然需要检查规格是否是您想要的,并且您没有错过任何意想不到的效果。


CharlieWang
20 声望2 粉丝

Be a cartoon heart