发送和接受ETH方式汇总

说明

在solidity中存在几个与接受和发送ETH相关的函数,修饰符。

payable

被payable修饰的方法或者是合约就能够接收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
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.13;

contract Payable {
// Payable address can receive Ether
address payable public owner;

// Payable constructor can receive Ether
constructor() payable {
owner = payable(msg.sender);
}

// Function to deposit Ether into this contract.
// Call this function along with some Ether.
// The balance of this contract will be automatically updated.
function deposit() public payable {}

// Call this function along with some Ether.
// The function will throw an error since this function is not payable.
function notPayable() public {}

// Function to withdraw all Ether from this contract.
function withdraw() public {
// get the amount of Ether stored in this contract
uint amount = address(this).balance;

// send all Ether to owner
// Owner can receive Ether since the address of owner is payable
(bool success, ) = owner.call{value: amount}("");
require(success, "Failed to send Ether");
}
}
  • owner,被payable修饰,就表示可以接收ETH
  • deposit(),被payable修饰,外界调用deposit()方法并携带ETH时,就会将eth全部放入到合约中
  • withdraw(),就是通过call()方法,将合约中的eth全部转入到owner中
  • notPayable(),没有被payable修饰,所以当调用notPayable()方法并且携带ETH时,就会出错

通过foundry部署这个项目,并进行测试:

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
37
38
39
40
41
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.13;

import "forge-std/Test.sol";
import "../src/Payable.sol";

contract PayableTest is Test {
Payable public payableContract;
address owner = 0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266;
function setUp() public {
vm.prank(owner);
payableContract = new Payable();
}

function testOwner() public {
address testAddress = 0x70997970C51812dc3A010C7d01b50e0d17dc79C8;

vm.deal(owner,0); // 清空owner的余额,保证是空
console2.log("owner balance",payableContract.owner().balance); // 0

vm.prank(testAddress);
// 存入1 eth
payableContract.deposit{value: 1 ether}();

// 查询当前合约余额
console2.log("after deposit,contract balance",address(payableContract).balance); // 1 ether
console2.log("after deposit,owner balance",payableContract.owner().balance); // 0

vm.prank(testAddress);
payableContract.withdraw(); // 调用withdraw方法,将合约中的eth全部转入到owner中

// 查询当前合约余额
console2.log("after withdraw,contract balance",address(payableContract).balance); // 0
console2.log("after withdraw,owner balance",payableContract.owner().balance); // 1 ether

vm.prank(testAddress);
payableContract.notPayable{value: 1 ether}(); // notPayable没有payable修饰,无法接受eth,程序出错

}

}

运行测试,orge test -vvv --rpc-url=http://127.0.0.1:8545。最终得到的结果如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
[PASS] testOwner() (gas: 36704)
Logs:
owner balance 0
after deposit,contract balance 1000000000000000000
after deposit,owner balance 0
after withdraw,contract balance 0
after withdraw,owner balance 1000000000000000000
````
和预期的结果一致。
至于`notPayable`,程序出错信息是`TypeError: Cannot set option "value" on a non-payable function type`,也和预期一致。

### Fallback

fallback在solidity中是一个内置函数,函数原型如下所示。fallback函数内部可以有一些内部的自定义功能,比如event事件日志等等。

fallback() external payable {
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

fallback,翻译成中文是回退的意思,可以理解为是整个合约的兜底方法。`fallback()`声明时不需要`function`关键字,必须由`external`修饰,一般也会用`payable`修饰用于接收ETH。一般的声明语句是`fallback() external payable { ... }`。当调用的合约方法同时满足以下的条件时,就会自动触发:

1. 调用的方法不存在
2. 调用的方法携带ETH
3. `receiver()`(后面会讲解receiver)方法不存在或者是msg.data不为空

一个简单的示例程序如下:

```solidity
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.13;

contract Fallback {
// Fallback function must be declared as external.
fallback() external payable {
}
}

通过foundry部署这个项目,并进行测试。如果直接在.t.sol中测试一个不存在的方法,编译不通过,所以尝试通过本地部署合约的方法来实现。

部署合约
部署Fallback合约
forge create --rpc-url http://127.0.0.1:8545 --private-key private_key src/Fallback.sol:Fallback
得到如下信息:

1
2
3
Deployer: 0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266
Deployed to: 0x5FbDB2315678afecb367f032d93F642f64180aa3 // 合约地址
Transaction hash: 0xdd637ed8664ddb2bdb188eae04bc4ec69d121df474b275d99cee69134411edb3

调用方法
尝试调用Fallback合约中不存在的方法并且携带eth,触发fallback函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
$ cast send 0x5FbDB2315678afecb367f032d93F642f64180aa3  "test()" --rpc-url http://127.0.0.1:8545 --private-key private_key --value 1ether

blockHash 0xbc5afedeac0a83f1525788905fe9b1a3bf62877ceb76c5e65bb6bcddcebf9544
blockNumber 3
contractAddress
cumulativeGasUsed 21082
effectiveGasPrice 3670614770
gasUsed 21082
logs []
logsBloom 0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
root
status 1
transactionHash 0x1062379c10c0900a1bd953080d3da9bd5030a315eacf8fc2f9335a88f36d3525
transactionIndex 0
type

其中--value 1ether,表示携带1个以太币。

查看合约余额

1
2
$cast balance 0x5FbDB2315678afecb367f032d93F642f64180aa3 --rpc-url http://127.0.0.1:8545
1000000000000000000 // 1 eth

我们调用了Fallback合约中不存在的方法并且携带了1 eth,最终成功触发了fallback()函数,Fallback合约成功获得1个eth。

receive

receive()只用于处理接收ETH。一个合约最多有一个receive()函数,声明方式与一般函数不一样,不需要function关键字:receive() external payable { ... }receive()函数不能有任何的参数,不能返回任何值,必须包含externalpayablereceive()被执行必须是没有call data的调用.

合约接收ETH的时候,receive()会被触发。receive()最好不要执行太多的逻辑因为如果别人用sendtransfer方法发送ETH的话,gas会限制在2300receive()太复杂可能会触发Out of Gas报错。receive()触发的条件是msg.data必须为空,否则就调用fallback()。示例合约如下:

1
2
3
4
5
6
7
8
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.13;

contract ReceiveEther {
// // Function to receive Ether. msg.data must be empty
receive() external payable {
}
}

测试方法和上面的Fallback测试方法稍有不同.
部署合约
部署Fallback合约

1
2
3
4
5
6
7
forge create --rpc-url http://127.0.0.1:8545 --private-key your-private-key src/ReceiveEther.sol:ReceiveEther

Compiler run successful
Deployer: 0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266
Deployed to: 0x5FbDB2315678afecb367f032d93F642f64180aa3 # 合约地址
Transaction hash: 0x360194e092027407d3d0fd43d3311b6cb6187c3f1a163b08279e0ac5067283cf

发送交易
如果要触发receive()方法,必须是没有call data的调用,所以使用cast send调用时,不能带有方法名.如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
cast send 0x5FbDB2315678afecb367f032d93F642f64180aa3  --rpc-url http://127.0.0.1:8545 --private-key your-private-key --value 2ether --gas-limit 7000000

blockHash 0x19c7988a83bbe209f9230e07d9d1a583044bf59968a9c4c5c0e16e06e8fbabd1
blockNumber 2
contractAddress
cumulativeGasUsed 21055
effectiveGasPrice 3766369786
gasUsed 21055
logs []
logsBloom 0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
root
status 1
transactionHash 0x7980a770a0d8f9ef7eac061c84066f2c5432a5ce9c454a4ef586d82fcfc1369b
transactionIndex 0
type

查看合约余额
在上一步交易中,我们向合约中发送了2 eth,如果成功调用了,那么合约中的余额也应该是2 eth.

1
2
cast balance 0x5FbDB2315678afecb367f032d93F642f64180aa3 --rpc-url http://127.0.0.1:8545
2000000000000000000 // 2 eth

合约中余额和我们预想的一致,说明当我们使用一个没有call data调用合约时,成功触发了合约中的receive()方法,转账成功.

receive和fallback的区别

两者虽然都可以接受ETH,但是还是存在区别.主要表现如下:

1
2
3
4
5
6
7
8
9
10
11
    send Ether
|
msg.data is empty?
/ \
yes no
/ \
receive() exists? fallback()
/ \
yes no
/ \
receive() fallback()

简单来说,合约接收ETH时,msg.data为空且存在receive()时,会触发receive();其余情况全部都是调用fallback().如果合约中同时不存在receive()fallback(),交易就会出错.

transfer

  • 用法: to.transfer(amount),to 是转账的接受地址,amount是转账的ETH的数量
  • transfer()的gas限制是2300,足够用于转账,但对方合约的fallback()receive()函数不能实现太复杂的逻辑
  • transfer()没有返回值无法表明转账结果.但是如果转账失败,会自动revert(回滚交易)。

为了方便测试,我们在ReceiveEther合约中添加一个getBalance()方法,用于获取合约中的余额.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.13;

contract ReceiveEther {
// Function to receive Ether. msg.data must be empty
receive() external payable {}

// Fallback function is called when msg.data is not empty
fallback() external payable {}

function getBalance() public view returns (uint) {
return address(this).balance;
}
}

这次直接使用forge test进行测试功能,测试代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.13;

import "forge-std/Test.sol";
import "../src/SendEthRecv.sol";

contract SendEthTest is Test {
ReceiveEther public receiveEther;
address payable public receiveEtherAddr;
address testaddress = 0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266;
function setUp() public {
receiveEther = new ReceiveEther();
receiveEtherAddr = payable(address(receiveEther));
}

function testSendViaTransfer() public {
vm.prank(testaddress);
receiveEtherAddr.transfer(1 ether); // transfer 1 ether to receiveEther
assertEq(receiveEther.getBalance(), 1 ether); // check receiveEther balance, should be 1 ether
}

}

最终的结果是:[PASS] testSendViaTransfer()
说明我们的transfer()方法是可以正常使用的,也是符合预期的

send

  • 用法: to.send(amount),to 是转账的接受地址,amount是转账的ETH的数量
  • send()的gas限制是2300,足够用于转账,但对方合约的fallback()receive()函数不能实现太复杂的逻辑
  • send()有返回值,返回值为true表示转账成功,返回值为false表示转账失败,所以程序根据返回值可以判断转账是否成功,后面进行进一步的分析
  • send() 如果转账失败,不会自动revert(回滚交易)。

由于测试合约和测试代码基本上和上面的transfer()一致,所以这里就不再赘述了,直接贴上测试代码:

1
2
3
4
5
6
function testSendViaSend() public {
vm.prank(testaddress);
bool sent = receiveEtherAddr.send(1 ether); // send 1 ether to receiveEther
assertEq(receiveEther.getBalance(), 1 ether); // check receiveEther balance, should be 1 ether
assertEq(sent, true);
}

最终的结果是:[PASS] testSendViaSend()
说明我们的send()方法是可以正常使用的,也是符合预期的

call

  • 用法: to.call{value: amount}(data),to 是转账的接受地址,amount是转账的ETH的数量,data是调用的数据
  • call()没有gas限制,可以支持对方合约fallback()或receive()函数实现复杂逻辑
  • call()如果转账失败,不会revert
  • call()的返回值是(bool, data),其中bool代表着转账成功或失败,需要额外代码处理一下

测试代码如下:

1
2
3
4
5
6
function testsendViaCall() public {
vm.prank(testaddress);
(bool sent, bytes memory data) = receiveEtherAddr.call{value: 1 ether}("");
assertEq(receiveEther.getBalance(), 1 ether); // check receiveEther balance, should be 1 ether
assertEq(sent, true);
}

最终的结果是:[PASS] testsendViaCall()
说明我们的call()正常调度,没有问题.

总结

本文主要是接受ETH的方法和发送ETH的方法进行了总结是实验.接受ETH的方法有fallback()receive().两者的使用任何的差别,相对而言,fallback()更加适用,从实际情况来看,fallback()相对receive()也更加常用.

发送ETH有三个方法,分别是transfer(),send()call()

  • call(),没有gas限制,最为灵活,是最提倡的方法,但是这个方法适合转账,不适合调用已经存在的方法
  • transfer(),有2300 gas限制,但是发送失败会自动revert交易,是次优选择
  • send()有2300 gas限制,而且发送失败不会自动revert交易

参考

  1. https://mirror.xyz/ninjak.eth/EroVZqHW1lfJFai3umiu4tb9r1ZbDVPOYC-puaZklAw
  2. https://solidity-by-example.org/sending-ether/
  3. https://solidity-by-example.org/fallback/
  4. https://www.youtube.com/watch?v=CMVC6Tp9gq4
  5. https://www.youtube.com/watch?v=mlPc3EW-nNA
文章作者: Oooverflow
文章链接: https://www.oooverflow.com/2022/09/28/recv-send-eth/
版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 Oooverflow