Solidity的fallback函数(二十三)|入门系列

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

fallback函数,回退函数,是合约里的特殊无名函数,有且仅有一个1。它在合约调用没有匹配到函数签名,或者调用没有带任何数据时被自动调用。

回退函数

回退函数是合约里的特殊函数,没有名字,不能有参数,没有返回值。下面来看一个简单的回退函数例子。

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,表示的是uin2561(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类型的默认空值),空数据。

这里需要特别注意的是:

  1. 如果我们要在合约中通过send()函数接收,就必须定义fallback函数,否则会抛异常。
  2. 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。

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

参考资料

友情链接: 区块链技术中文社区