1.发账
发账有三种方式:transfer
, send
和call
。
(1) transfer
函数原型:
<address payable>.transfer(uint256 amount)
- 如果异常会转账失败,抛出异常(等价于require(send()))(合约地址转账)
- 有gas限制,最大2300
一般比较推荐transfer,因为相比send它有报错提示,而且可以回滚操作
!这是最重要的。下面还会举例子说明,send的风险。
(2) send
函数原型:
<address payable>.send(uint256 amount) returns (bool)
- 如果异常会转账失败,仅会返回false,不会终止执行(合约地址转账)
- 有gas限制,最大2300
send存在一个漏洞:
function sendToWinner() public {
require(!payedOut);
winner.send(winAmount);
payedOut = true;
}
比如这个方法,当使用send转账之后,由于没有进行require检查,无论成功或者失败,都会继续执行payedOut = true
,这叫造成我们并不想要的结果了。
所以说send和transfer,还是用transfer,风险低,可以自动回滚操作。
当然,如果你不想回滚操作,要针对转账结果进行一番操作的话,那就还是用send,如下:
bool succeed = account.send(1 ether);
if(succeed) {
......
} else {
......
}
(3) call
函数原型:
<address>.call(bytes memory) returns (bool, bytes memory)
- 如果异常会转账失败,仅会返回false,不会终止执行(调用合约的方法并转账)
- 没有gas限制
示例:
addr.call{value: 1 ether}("");
由于call转账的特点,非常容易造成重入攻击。详情可以看这篇文章《合约安全:重入漏洞》。
综上所述,最好的转账函数还是transfer
。
2.收账
(1)简介
合约收账主要有三种方式:通过普通payable函数
、payable fallback函数
和receive函数
。
普通payable函数
如A合约中,定义了payable函数recv()如下:
contract A {
function recv() external payable {};
}
假设A合约有示例a,那么我们调用时转账就可以这样写:
a.recv{value: msg.value}();
这样我们就可以发送msg.value
的以太去A合约上了。
receive和fallback
而当我们使用call
方法进行转账的时候,
_to.call{value: msg.value} ("...msg.data...")
根据上述代码中的msg.data
的不同,严格意义上有三种途径:
- 通过被调用合约上的payable方法
- 通过payable fallback
- 通过receive
其中通过合约上的payable方法,跟直接函数调用差不多,假设A的地址为_to,则如下:
_to.call{value: msg.value} (abi.encodeWithSignature("recv()"))
这样就实现了通过被调用合约上的payable方法recv()
进行收账,本质上跟直接函数调用差不多,这不是我们说的重点,主要的重点在与receive
和fallback
。
关于receive和fallback,我们在《Solidity的fallback和receive》聊得比较详细,至于这两者的选择和优先级的问题,有以下的调用规则:

在下面的示例中,我们会详细探讨这个问题。
(2)示例
有收账合约ReceiveEther
、转账合约SendEther
如下:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.7;
contract ReceiveEther {
event Fallback(bytes cdata, uint value, uint gas);
event Receive(uint value, uint gas);
event Foo(bytes cdata, uint value, uint gas);
function getBalance() public view returns (uint) {
return address(this).balance;
}
function foo() public payable {
emit Foo(msg.data, msg.value, gasleft());
}
fallback() external payable {
emit Fallback(msg.data, msg.value, gasleft());
}
receive() external payable {
emit Receive(msg.value, gasleft());
}
}
contract SendEther {
function sendViaCall1(address payable _to) external payable {
(bool sent, ) = _to.call{value: msg.value}("");
require(sent, "Failed to send Ether");
}
function sendViaCall2(address payable _to) external payable {
(bool sent, ) = _to.call{value: msg.value}("asdsa");
require(sent, "Failed to send Ether");
}
function sendViaFoo(address payable _to) external payable {
ReceiveEther re = ReceiveEther(_to);
re.foo{value: msg.value}();
}
}
首先来看看ReceiveEther
合约:
根据我们前文《Solidity的fallback和receive》的学习,我们知道当call
方法携带的msg.data
为空的时候,就会去执行receive函数。所以这里我们在ReceiveEther的合约中,可以看到Receive事件相比Fallback事件少了bytes cdata
的变量。而且其实在receive函数使用msg.data
的话编译器也会主动报错。
这里我们主要展示了接收方的三种接受转账的途径:
- 通过receive()
- 通过payable fallback()
- 通过常规payable函数foo()
再看看SendEther
合约:
对应接收方,我们发送方也有三种发账方式:
- call附带
msg.value
,设置msg.data
为""
,以此触发接收方的receive
- call附带
msg.value
,设置msg.data
为无意义字符串"asdsa"
,以此触发接收方的fallback
- 直接调用接收方的
foo
方法,附带msg.value
网友评论