Solidity发送ether最佳实践(二十四)|入门系列

2017/6/3 posted in  Solidity入门系列

发送ether的最佳实践1

send()与fallback():

Solidity中当签名不匹配任何的函数方法时,将会触发回退函数。比如,当你调用address.call(bytes4(bytes32(sha3("thisShouldBeAFunction(uint,bytes32)"))), 1, "test")时,EVM实际尝试调用地址中的thisShouldBeAFunction(),当这个函数不存在,会触发fallback函数。

由于send()函数指定了一个空函数签名,所以当fallback函数存在时,它总是会调用它。

下面是一个例子(来自:https://github.com/ethereum/wiki/wiki/Solidity-Tutorial#fallback-functions):

contract Test {
  function() { x = 1; }
  uint x;
}

contract Caller {
  function callTest(address testAddress) {
    Test(testAddress).call('0xabcdefgh'); // hash does not exist
    // results in Test(testAddress).x becoming == 1.
  }
}

可能的滥用

现在假设你有下述的情景:

假设你有一个基于以太坊的公司,现在需要向用户分红。为了简单,比如100个用户,平均分好,现在打算发送给他们每个人。当使用send()时,会触发fallback函数,可能会导致消耗许多的gas,损失公司所赚取的利润。原因是因为在EVM中,正常地址和账户地址之间没有任何区别。大多数的发送也是正常的,但比如到第11个用户时,这个用户可以实现一段恶意的代码,消耗完所有的gas,导致交易失败,其它所有人都没有支付成功。

这段恶意代码,可以简单的实现为,使用一个合约帐户,并在合约账户的回退函数中实现一个无限循环。所以除非我们移除第11个股东,否则我们的gas总会被耗尽从而导致失败。

修复方式

根本问题还是在于在以太坊上不能单方面相信其它的代码,尤其是当使用send()函数时,当前的解决方案如下:

  • Send()函数不再转发gas。它简单的从总的转帐gas花费(最低9040)中抽取硬编码的专用资金(2300 gas),用于发送ether。这基本够用,且还能做一个额外的小的日志记录操作。你将不能执行另一次转帐(因为最低消耗9040 gas)。同时你也不能做比如存储变量相关的操作。

  • send()调用消耗掉所有的gas时,它也不会抛出异常,只是返回false。

由此你可以在上述场景中安全的使用send()函数了。因为恶意用户即便用完了限定的专用gas,但函数只会返回false,所有其它用户的交易将能正常执行,均能正常收到支持。

额外的问题

然而,这将引发一个新的问题。

有些时间,在合约中就是要在收到ether后进行一些转发。使用send()和fallback函数的模式是受限的,你不能创建嵌套发送。换句话说,比如,tx.origin向合约A发送ether,合约A要转发给合约B,合约B还要转发给合约C等等。这将不可能,仅仅只有2300gas的专用gas,完全不足以发起另一个send()

解决方案

address.call.value()

使用call()调用是一个解决办法,使用call()时可以附带ether,同时它也会转发gas。所以可以通过address.call.value(ether to send)()模拟send()函数(这里比较巧妙,send方法默认调用空函数签名,这里直接模拟一个空函数签名调用)。

这将可以向任意地址发送ether,如果有fallback函数,也将会触发。也许你已经意识到,这将引发上述所说的漏洞,一个长的循环就将消耗掉所有的gas。

比如下面的代码就将消耗50000000gas:

 contract Gas_Loop {

    function() {
        for(uint i = 0; i < 10000; i+=1) {
            out_i = i;
        }
    }

    uint public out_i;
 }

在Solidity中,发起一个call()调用时,不指定gas,默认将占用34050来为后续的操作中使用。操作中剩余的gas将会继续向后转发。如果后续的执行中没有用光所有的gas,它们将被退回。如果你指定了特定数量的gas,比如address.gas(20000).value(1).call()(),那么后续操作中将只会携带对应数量的gas。

为每一次转帐指定一个gas花费是可行的,如果你知道后续调用的嵌套层次,会发生多少次转帐。不过一般来说,做到这些比较困难。

其它

如果系统使用fallback函数,和多层嵌套的转帐,同时依赖每个转帐点的转帐成功。那么可以考虑将系统进行扁平化的设计,所有的转帐都有顶层发起。比如由tx.origin -> send() -> send() -> send() -> send() -> etc调整为:

                                    /-> send()
tx.origin -> doCrawlOfWhomToPay() -> send()
                                   \-> send()
                                    |-> send()

代码审查

另一种代替address.call.value()()这种不是非常安全的解决方案的办法,开发一个函数(当然这个函数不会消耗尽gas),比如叫safeSend(uint _wei),让其映射到特定的字节码。函数里,会通过getCode,校验这段字节码是否存在(address.code 已经计划在Solidity中实现),如果存在才调用。

这是初步的,更多的探索。 比如使用潜在的附加服务,结合信誉系统+注册管理机构,在合约代码执行前,通过服务判断合约的安全性,以决定调不调用。

预言机

当使用send()函数时,由于基本上来说,有足够的gas来记录你收到的ether。那么可以实现一个预言机,来监测这个行为,一旦发生,再发起一个对应的交易,提供对应的gas,本质上也可以实现继续发送的目的。

关于作者

专注基于以太坊(Ethereum)的相关区块链(Blockchain)技术,了解以太坊,Solidity,Truffle,web3.js。

个人博客: http://tryblockchain.org
版权所有,转载注明出处

参考资料

处于某些特定的环境下,可以看到评论框,欢迎留言交流^_^。

友情链接: 区块链技术中文社区    深入浅出区块链