接收以太币 ETH
本章讲解在 Solidity
中,智能合约如何接收以太币 ETH
。
智能合约可以用来存储以太币 ETH
,它可以接收外部账户发来的 ETH。但是,并不是任何合约都具备接收 ETH 的能力。
一个智能合约想要接收 ETH
,就必须实现 receive
或者 fallback
函数。
我们在学习智能合约的接收函数 receive
之前,必须先理解以太坊中的账户类型。如果一个智能合约中这两个函数都没有定义,那么它就不能接收以太币。
按照 solidity
语言规范,推荐使用 receive
函数。因为 receive
函数简单明了、目的明确,而 fallback
函数主要用来处理未知函数时调用。
1. receive 函数
定义 receive
函数的格式如下:
receive() external payable { // 这里可以添加自定义的处理逻辑,但也可以为空 }
receive
函数有如下几个特点:
- 1)无需使用
function
声明。 - 2)参数为空。
- 3)可见性必须设置为
external
。 - 4)状态可变性必须设置为
payable
。
当外部地址向智能合约地址发送以太币时,将触发执行 receive
函数。
我们可以在函数体内不写任何自定义的处理逻辑,它依然能够接收以太币,这也是最常见的使用方式。
如果必须在 receive
的函数体内添加处理语句的话,最好不要添加太多的业务逻辑。
因为外部调用 send
和 transfer
方法进行转账的时候,为了防止重入攻击,gas
会限制在 2300。如果 receive
的函数太复杂,就很容易会耗尽 gas
,从而触发交易回滚。
receive
函数里通常会执行一些简单记录日志的动作,比如触发 event
。
// SPDX-License-Identifier: MIT pragma solidity ^0.8.0; contract FuncReceive { // 定义接收事件 event Received(address sender, uint amount); // 接收 ETH 时,触发 Received 事件 receive() external payable { emit Received(msg.sender, msg.value);
} }
我们把合约代码复制到 Remix
,进行编译,并部署到区块链上:
部署完成后,在 Value
中输入要存入的 ETH
数量,比如 100 wei
。然后点击下方的 Transact
,就会把当前账户中的 100 wei
转入到合约中,你可以看到当前合约的 Balance
变为 0.0000000000000001 ETH
。 另外,查看右下方控制台的输出日志中,我们可以看到 Received
事件被触发。
2. fallback 函数
在 Solidity
语言中,fallback
是一个预定义的特殊函数,用于在处理未知函数和接收以太币 ETH
时调用。
定义 fallback
函数的格式如下:
fallback () external [payable] { // 这里可以添加自定义的处理逻辑,但也可以为空 }
fallback
函数有如下几个特点:
- 1)无需使用
function
声明。 - 2)参数为空。
- 3)可见性必须设置为
external
。 - 4)状态可变性可以为空,或者设置为
payable
。
2.1 fallback 调用条件
fallback
会在两种情况下,被外部事件触发而执行:
1) 外部调用了智能合约中不存在的函数
在这种情况下,函数声明中无需设置状态可变性,函数形式如下:
fallback () external { }
2) 外部向智能合约中存入以太币,并且当前合约中不存在 receive 函数
在这种情况下,函数声明中必须设置状态可变性为 payable
,函数形式如下:
fallback () external payable { }
如果合约中已经定义了 receive
函数,那么向这个合约中存入以太币,将会优先调用 receive
函数,而不会执行 fallback
函数。
所以,如果一个智能合约允许存入以太币,那么它就必须实现 receive
或者 fallback
函数,而且函数的状态可变性设置为 payable
。
如果一个智能合约没有定义这两个函数中的任何一个,那么它就不能接收以太币。
2.2 receive 和 fallback 工作流程
receive
和 fallback
的触发条件,可以参考以下流程:
当参数 msg.data
为空时,就意味着:外部向合约进行转账,存入以太币。
当参数 msg.data
不为空时,就意味着:外部在调用合约中的函数。
我们在左边的分支可以看到,receive
和 fallback
函数都能够用于接收以太币 ETH
。
一个智能合约在接收 ETH
时:
- 如果存在着
receive
函数,就会触发receive
; - 当不存在
receive
函数,但存在fallback
函数时,就会触发fallback
; - 而当两者都不存在时,交易就会
revert
,存入ETH
失败。
2.3 测试和验证
我们来编写几个合约来测试和验证 fallback
函数。
a) 第一种情况
智能合约中只定义 fallback
函数,而且状态可变性为 payable
。
// SPDX-License-Identifier: MIT pragma solidity ^0.8.0; contract FuncFallback { // 定义回退事件 event Fallback(); fallback() external payable { emit Fallback(); } }
当我们向合约中存入以太币时,将会执行 fallback
函数,从而触发里面的 Fallback
事件。
我们把这个合约部署在 Remix
上。然后,在 "VALUE" 栏中填入要发送给合约的金额(单位是 Wei),再点击 "Transact", 存入以太币。
我们可以看到交易成功,并且触发了 Fallback
事件。
b) 第二种情况
智能合约中同时定义了 receive
和 fallback
函数,而且两者的状态可变性都为 payable
。
// SPDX-License-Identifier: MIT pragma solidity ^0.8.0; contract FuncFallback { // 定义接收事件 event Receive(); // 定义回退事件 event Fallback(); receive() external payable { emit Receive(); } fallback() external payable { emit Fallback(); } }
当我们向合约中存入以太币时,将会执行 receive
函数,从而触发里面的 Receive
事件。
我们把这个合约部署在 Remix
上。然后,在 "VALUE" 栏中填入要发送给合约的金额,再点击 "Transact", 存入以太币。
我们可以看到交易成功,并且触发了 Receive
事件,但没有触发 Fallback
事件。
c) 第三种情况
依然使用上面的合约,当我们调用一个不存在的函数,将会触发 Fallback
事件。
// SPDX-License-Identifier: MIT pragma solidity ^0.8.0; contract FuncFallback { // 定义接收事件 event Receive(); // 定义回退事件 event Fallback(); receive() external payable { emit Receive(); } fallback() external payable { emit Fallback(); } }
我们在 "CALLDATA" 栏中填入随意编写的 msg.data
数据,使之不为空,再点击 "Transact"。
我们可以看到交易成功,并且触发了 Fallback
事件。
2.4 fallback 使用场景
按照 Solidity
语言新的规范,如果只是为了让合约账户能够存入以太币,推荐使用 receive
函数,而不是使用 fallback
函数。
这样做的好处就是,从函数命名就可以知其用途,职责划分明确,防止引起混乱,导致误用。
receive
函数,只用于接收以太币。而 fallback
,只用于调用了不存在的合约函数。
fallback 和 receive 函数的使用场景:
a) 空投
利用 receive
或者 fallback
函数,用户只需要使用钱包向空投合约发送 0 金额的转账,空投合约就可以向该地址进行空投。
b) 锁仓
用户使用钱包将代币转账到锁仓合约中,锁仓合约利用 receive
或者 fallback
函数接收到请求,就可以执行锁仓逻辑了。
c) 兑换
在 ERC20
代币 WETH
合约中,利用 receive
或者 fallback
函数,在收到 ETH
后,自动兑换为 WETH
代币。
关于这方面的应用,可以参考 BinSchool
网站中《Solidity 常用合约》章节。