0%

solidity | 回退函数、receive和fallback

  • 回退函数
  • receive
  • fallback

回退函数 「在 0.6 版本之后失效,变成 receive和fallback」

  • 无名称
  • 无参数
  • 无返回值
  • 一个合约只能有一个回退函数
  • 当给合约转以太币的时候,需要 payable 回退函数
  • 如果调用合约没有匹配上任何函数,就会调用回退函数
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
pragma solidity ^0.4.18;

contract Test{

uint public x;

function() public payable{
x = 1;
}

}

contract Call{

constructor() public payable{}

function() public payable{
}

function tansferTest(address addr) public {
addr.transfer(1 ether);
}
}

上面的函数在执行 call.transferTest 的时候会失败。

因为,回退函数在接收以太币的时候,仅有 2300 gas 来执行,下面的操作超过 2300

  • 写存储
  • 创建合约
  • 执行一个外部函数调用
  • 发送 ether

receive()和fallback(),他们主要在两种情况下被使用:

  • 接收ETH
  • 处理合约中不存在的函数调用(代理合约 proxy contract

注意:在 solidity 0.6.x 版本之前,语法上只有 fallback() 函数,用来接收用户发送的 ETH 时调用以及在被调用函数签名没有匹配到时,来调用。

0.6 版本之后,solidity 才将 fallback() 函数拆分成 receive()fallback() 两个函数。

我们这一讲主要讲接收 ETH 的情况。

接收ETH函数 receive

receive() 只用于处理接收 ETH

一个合约最多有一个 receive() 函数,声明方式与一般函数不一样,不需要 function 关键字:receive() external payable { ... }

receive() 函数不能有任何的参数,不能返回任何值,必须包含 externalpayable

当合约接收 ETH 的时候,receive() 会被触发。

receive() 最好不要执行太多的逻辑因为如果别人用 sendtransfer 方法发送 ETH 的话,gas 会限制在 2300receive() 太复杂可能会触发Out of Gas 报错;如果用 call 就可以自定义 gas 执行更复杂的逻辑。

我们可以在 receive() 里发送一个 event,例如:

1
2
3
4
5
6
// 定义事件
event Received(address Sender, uint Value);
// 接收ETH时释放Received事件
receive() external payable {
emit Received(msg.sender, msg.value);
}

有些恶意合约,会在 receive() 函数(老版本的话,就是 fallback() 函数)嵌入恶意消耗 gas 的内容或者使得执行故意失败的代码,导致一些包含退款和转账逻辑的合约不能正常工作,因此写包含退款等逻辑的合约时候,一定要注意这种情况。

回退函数 fallback

fallback() 函数会在调用合约不存在的函数时被触发。可用于接收 ETH ,也可以用于代理合约 proxy contract

fallback() 声明时不需要 function 关键字,必须由 external 修饰,一般也会用 payable 修饰,用于接收 ETH: fallback() external payable { ... }

我们定义一个 fallback() 函数,被触发时候会释放 fallbackCalled 事件,并输出 msg.sendermsg.valuemsg.data:

1
2
3
4
// fallback
fallback() external payable{
emit fallbackCalled(msg.sender, msg.value, msg.data);
}

receive和fallback的区别

receivefallback 都能够用于接收ETH,他们触发的规则如下:

1
2
3
4
5
6
7
8
9
10
11
12
触发fallback() 还是 receive()?
接收ETH
|
msg.data是空?
/ \
是 否
/ \
receive()存在? fallback()
/ \
是 否
/ \
receive() fallback()

简单来说,合约接收 ETH 时,msg.data为空且存在 receive() 时,会触发receive()msg.data不为空或不存在receive()时,会触发fallback(),此时fallback()必须为payable

receive()payable fallback()均不存在的时候,向合约直接发送ETH将会报错(你仍可以通过带有payable的函数向合约发送ETH)。

ps: 2023-2-24 ,如果 msg.data 为空,且 receive 不存在,此时,只有 fallback 会报错,而不是图中所示。所以,receivefallback 都需要实现。

案例

成功 「receive」

代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
pragma solidity ^0.8.0;

contract test{
constructor() payable{

}

function transferEth(address payable T) public{
T.transfer(100);
}

function getBlanace() public view returns(uint256){
return address(this).balance;
}
}

contract callfunction {
event reveiveCalled(address);
event fallbackCalled(address,uint256,bytes);

receive() external payable{
emit reveiveCalled(msg.sender);
}

fallback() external payable{
emit fallbackCalled(msg.sender, msg.value, msg.data);
}

function getBlanace() public view returns(uint256){
return address(this).balance;
}
}

由于 remix 自带的链上操作,没有转账功能,所以,创建了一个合约进行转账。

  • 先部署 test 「传入 1ETH
  • 再部署 callfunction
  • 执行 callfunctiongetBlanace
    • 0
  • 执行 testtransferEth(callfunction 的地址)
  • 执行 callfunctiongetBlanace
    • 100
    • log 输出
    • 说明执行的是 receive()

log 输出

1
2
3
4
5
6
7
8
9
10
[
{
"from": "0xb27A31f1b0AF2946B7F582768f03239b1eC07c2c",
"topic": "0x794ae4db4fa93171e957bf40514fe3fd673f9c8a166e172f0ffb677b53b55648",
"event": "reveiveCalled",
"args": {
"0": "0xcD6a42782d230D7c13A74ddec5dD140e55499Df9"
}
}
]

如果想要回退 ETH 可以使用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
pragma solidity ^0.8.0;

contract test{
constructor() payable{

}

function transferEth(address payable T) public{
T.transfer(100);
}

function getBlanace() public view returns(uint256){
return address(this).balance;
}
}

contract callfunction {

event reveiveCalled(address);
event fallbackCalled(address,uint256,bytes);

receive() external payable{
emit reveiveCalled(msg.sender);
revert();
}

fallback() external payable{
emit fallbackCalled(msg.sender, msg.value, msg.data);
}

function getBlanace() public view returns(uint256){
return address(this).balance;
}
}

这样就不会向 callfunction 转移 ETH 了。

失败 「receive」

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
pragma solidity ^0.8.0;

contract test{
constructor() payable{

}

function transferEth(address payable T) public{
T.transfer(100);
}

function getBlanace() public view returns(uint256){
return address(this).balance;
}
}

contract callfunction {

uint256 x;

event reveiveCalled(address);
event fallbackCalled(address,uint256,bytes);

receive() external payable{
x = 200;
emit reveiveCalled(msg.sender);
}

fallback() external payable{
emit fallbackCalled(msg.sender, msg.value, msg.data);
}

function getBlanace() public view returns(uint256){
return address(this).balance;
}
}

由于赋予 xgas 超过了 2300,所以,receive 会失败。

失败后

  • test 合约
    • ETH 数量没有变化
  • callfunction 合约
    • balance = 0

成功 「fallback」

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
pragma solidity ^0.8.0;

contract test{
constructor() payable{

}

function transferEth(address payable T) public{
T.transfer(100);
}

function getBlanace() public view returns(uint256){
return address(this).balance;
}
}

contract callfunction {

event fallbackCalled(address,uint256,bytes);

fallback() external payable{
emit fallbackCalled(msg.sender, msg.value, msg.data);
}

function getBlanace() public view returns(uint256){
return address(this).balance;
}
}

执行操作如下

ps: msg.data 不是 inputs

log 输出

1
2
3
4
5
6
7
8
9
10
11
12
[
{
"from": "0x9396B453Fad71816cA9f152Ae785276a1D578492",
"topic": "0x7bf8121d238f1338d4842c396807cbe93f5a2396e2b3cad1639735997867b1f5",
"event": "fallbackCalled",
"args": {
"0": "0x5B38Da6a701c568545dCfcB03FcB875f56beddC4",
"1": "19",
"2": "0xabcd"
}
}
]
请我喝杯咖啡吧~