fallback函数,回退函数,是合约里的特殊无名函数,有且仅有一个
回退函数
回退函数是合约里的特殊函数,没有名字,不能有参数,没有返回值。下面来看一个简单的回退函数例子。
pragma solidity ^0.4.0;
contract SimpleFallback{
function(){
//fallback function
}
}
调用函数找不到时
当调用的函数找不到时,就会调用默认的fallback函数。由于Solidity中,Solidity提供了编译期检查,所以我们不能直接通过Solidity调用一个不存在的函数。但我们可以使用Solidity的提供的底层函数address.call来模拟这一行为,关于call()函数详见:http://me.tryblockchain.org/Solidity-call-callcode-delegatecall.html 。我们来看个例子:
pragma solidity ^0.4.0;
contract ExecuteFallback{
//回退事件,会把调用的数据打印出来
event FallbackCalled(bytes data);
//fallback函数,注意是没有名字的,没有参数,没有返回值的
function(){
FallbackCalled(msg.data);
}
//调用已存在函数的事件,会把调用的原始数据,请求参数打印出来
event ExistFuncCalled(bytes data, uint256 para);
//一个存在的函数
function existFunc(uint256 para){
ExistFuncCalled(msg.data, para);
}
// 模拟从外部对一个存在的函数发起一个调用,将直接调用函数
function callExistFunc(){
bytes4 funcIdentifier = bytes4(keccak256("existFunc(uint256)"));
this.call(funcIdentifier, uint256(1));
}
//模拟从外部对一个不存在的函数发起一个调用,由于匹配不到函数,将调用回退函数
function callNonExistFunc(){
bytes4 funcIdentifier = bytes4(keccak256("functionNotExist()"));
this.call(funcIdentifier);
}
}
在上面的代码中,我们定义了一个fallback函数,和一个对应的显示请求原始数据的事件FallbackCalled。
当我们调用callExistFunc()时,由于函数实际存在,会直接触发existFunc()的调用,我们能看到ExistFuncCalled事件被触发,运行时将打印出ExistFuncCalled[ "0x42a788830000000000000000000000000000000000000000000000000000000000000001","1"]。其中第一个数据是调用该函数时,传过来的原始数据,前四个字节42a78883,是existFunc()的方法签名,指明是对该函数进行调用,紧跟其后的是函数的第一个参数0000000000000000000000000000000000000000000000000000000000000001,表示的是uin256值1(32字节的无符号整数值十六进制表示),数据格式说明详见:http://me.tryblockchain.org/Solidity-abi-abstraction.html。
当我们调用的函数找不到时才会触发对fallback函数的自动调用。当调用callNonExistFunc(),由于它调用的functionNotExist()函数在合约中实际并不存在。故而,实际会触发对fallback函数的调用,运行后会触发FallbackCalled事件,说明fallback被调用了。事件输出的数据是,FallbackCalled[ "0x69774a91"],0x69774a91是调用的原始数据,是调用的functionNotExist()函数的四字节的函数签名。
send()函数发送ether
当我们使用address.send(ether to send)向某个合约直接转帐时,由于这个行为没有发送任何数据,所以接收合约总是会调用fallback函数,我们来看看下面的例子:
pragma solidity ^0.4.0;
contract SendFallback{
//fallback函数及其事件
event fallbackTrigged(bytes data);
function() payable{fallbackTrigged(msg.data);}
//存入一些ether用于后面的测试
function deposit() payable{
}
//查询当前的余额
function getBalance() constant returns(uint){
return this.balance;
}
event SendEvent(address to, uint value, bool result);
//使用send()发送ether,观察会触发fallback函数
function sendEther(){
bool result = this.send(1);
SendEvent(this, 1, result);
}
}
在上述的代码中,我们先要使用deposit()合约存入一些ether,否则由于余额不足,调用send()函数将报错。存入ether后,我们调用sendEther(),使用send()向合约发送数据,将会触发下述事件:
SendEvent[
"0xc35f7ac1351648b0b8a699c5f07dd6a78f626714",
"1",
"true"
]
fallbackTrigged[
"0x"
]
可以看到,我们成功使用send()发送了1wei到合约,触发了fallback函数,附带的数据是0x(bytes类型的默认空值),空数据。
这里需要特别注意的是:
如果我们要在合约中通过send()函数接收,就必须定义fallback函数,否则会抛异常。
fallback函数必须增加payable关键字,否则send()执行结果将会始终为false。
fallback中的限制
send()函数总是会调用fallback,这个行为非常危险,著名的DAO被黑也与这有关。如果我们在分红时,对一系列帐户进行send()操作,其中某个做恶意帐户中的fallback函数实现了一个无限循环,将因为gas耗尽,导致所有send()失败。为解决这个问题,send()函数当前即便gas充足,也只会附带限定的2300gas,故而fallback函数内除了可以进行日志操作外,你几乎不能做任何操作。如果你还想做一些复杂的操作,解决方案看这里:http://me.tryblockchain.org/blockchain-solidity-fallback-bestpractice.html。
下述行为消耗的gas都将超过fallback函数限定的gas值:
向区块链中写数据
创建一个合约
调用一个external的函数
发送ether
所以一般,我们只能在fallback函数中进行一些日志操作:
pragma solidity ^0.4.0;
contract FallbackFailOnGasLimit{
uint someStorage;
event fallbackTrigged(bytes);
function() payable{
fallbackTrigged(msg.data);
//将因为写入操作失败,注释掉下面这行,将会执行成功
someStorage = 1;
}
function callFallback() returns (bool){
return this.send(0);
}
}
在上述代码中,fallback函数有写入操作,会消耗掉超过限定的gas,故会导致失败,注释掉someStorage = 1;后,执行callFallback()将会成功。
注意:上述仅对使用send()方式的有2300gas的限制,对使用call()方式没有这样的限制。
关于作者
专注基于以太坊(Ethereum)的相关区块链(Blockchain)技术,了解以太坊,Solidity,Truffle,web3.js。
参考资料
处于某些特定的环境下,可以看到评论框,欢迎留言交流^_^。