回退函数与三种方法发送ETH
之前测试过重入漏洞,了解到发送主币 ETH 给一个合约,这个合约需要有一个回退函数,如果发送币使用 call 容易造成重入的问题,这次我们来详细了解回退函数,还有三种方法发送 ETH,分别看它们之间有什么不同点。
回退函数
回退函数有两个功能,一个是用于接收主币 ETH,还有一个作用是当调用的函数在合约里不存在,会调用回退函数。回退函数有两种,一种是 receive,一种是 fallback,这两种都可以用于接受主币,那它们有什么区别呢?
当以太坊的主币发送到合约,会先判断 msg.data 是否为空,如果不为空会调用 fallback,如果为空会先判断判断 receive 是否存在,如果存在则调用 receive,不存在则调用 fallback。下面我们编写一个 Test 合约来测试,代码里有 fallback 和 recive 两个回退函数,都添加了一个 Log 日志,日志会返回 4 个参数,第一个是函数名,第二个是发送者地址,第三个是发送的数量,第四个是 bytes 数据,可以用于合约之间的交互,代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
// SPDX-License-Identifier: MIT pragma solidity 0.8.7; contract Test { event Log(string functionName, address sender, uint value, bytes data); fallback() external payable { emit Log("fallback", msg.sender, msg.value, msg.data); } receive() external payable { emit Log("receive", msg.sender, msg.value, ""); } } |
布署 Tes 合约后,测试发送 1 个 ETH,在 CALLDATA 数据里随便输入一段数据,比如 0x112233,点击 Transact 即可发送,如下图所示:
从日志里可以看到,会有 4 个参数,其中 functionName 是 fallback,value 是 数据,data 是 CALLDATA 里输入的数据,如下图所示,说明 msg.data 有数据的话,就会调用 fallback,主要为了合约之间的交互 。
再将 CALLDATA 置空,充值一个 ETH,这时发现 functionName 是 receive,说明在 msg.data 为空的情况下,会调用 receive,如下图所示。当然如果合约里没有 receive,还是会调用 fallback。
三种方法发送ETH
solidity 有三种发送 ETH 的方法,分别是 transfer、send、call。
(1) transfer 只会带有 2300 个 gas,没有返回值,如果失败会提示 reverts 错误。
(2) send 也是只带有 2300 个 gas,但是会有返回值,返回 true 代表成功,返回 false 代表失败。
(3) call 会发送所有剩余的 gas,会有两个返回值,第一个是否成功的 bool 值,第二个是一个 bytes 数据,如果发送的目标是一个合约,这个合约有返回值,就会返回在这个 bytes 数据里,主要用于合约之间的数据交互。
下面编写测试代码看看效果,编写一个名为 SendETH 的合约, 在构造函数里添加 payable 属性,方便在创建时得到 ETH,然后添加三个函数 Transfer、Send,Call,分别用来测试三种方式发送 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 |
pragma solidity 0.8.7; contract SendETH { constructor() payable{} function Transfer(address payable _to) external payable{ _to.transfer(1); } function Send(address payable _to) external payable{ bool sent = _to.send(1); require(sent, "send failed"); } function Call(address payable _to) external payable{ (bool sent,) = _to.call{value: 1}(""); require(sent, "call failed"); } function getBalance() external view returns(uint){ return address(this).balance; } } |
再编写一个 Test 合约,用来接受 ETH,添加 receive 回退函数,再添加一个 Log 日志,日志里有两个参数,第一个是打印函数名称,第二个是调用 gasleft 获取剩余的 gas,代码如下:
1 2 3 4 5 6 7 8 9 10 11 |
contract Test { event Log(string functionName, uint gas); receive() external payable { emit Log("receive", gasleft()); } function getBalance() external view returns(uint){ return address(this).balance; } } |
接下来布署两个合约,在 SendETH 合约布署时,添加一个 ETH,如下图所示:
先使用 transfer 方法发送,查看打印的日志,可以发现剩余的 gas 是 2240,如下图所示,使用 send 方法发送,gas 也是一样。
再测试使用 call 方法发送,从日志打印可以发现剩余的 gas 是 7216,这样如果在回退函数还可以再执行其他操作,容易造成重入,如下图所示。
转载请注明:exchen's blog » 回退函数与三种方法发送ETH