说明 在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()函数不能有任何的参数,不能返回任何值,必须包含external和payable。receive()被执行必须是没有call data的调用.
合约接收ETH的时候,receive()会被触发。receive()最好不要执行太多的逻辑因为如果别人用send和transfer方法发送ETH的话,gas会限制在2300,receive()太复杂可能会触发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交易
参考
https://mirror.xyz/ninjak.eth/EroVZqHW1lfJFai3umiu4tb9r1ZbDVPOYC-puaZklAw
https://solidity-by-example.org/sending-ether/
https://solidity-by-example.org/fallback/
https://www.youtube.com/watch?v=CMVC6Tp9gq4
https://www.youtube.com/watch?v=mlPc3EW-nNA