发送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
版权所有,转载注明出处
参考资料
处于某些特定的环境下,可以看到评论框,欢迎留言交流^_^。