solidity-接收和发送eth
接收eth
receive
Solidity支持两种特殊的回调函数,receive()和fallback(),他们主要在两种情况下被使用:
- 接收ETH
- 处理合约中不存在的函数调用(代理合约proxy contract)
注意⚠️:在solidity 0.6.x版本之前,语法上只有 fallback() 函数,用来接收用户发送的ETH时调用以及在被调用函数签名没有匹配到时,来调用。 0.6版本之后,solidity才将 fallback() 函数拆分成 receive() 和 fallback() 两个函数。
我们这一讲主要讲接收ETH的情况。
receive() 只用于处理接收ETH。一个合约最多只能有一个 receive() 函数,声明方式与一般函数不一样,不需要 function 关键字:receive() external payable { ... }
。receive()函数不能有任何的参数,不能返回任何值,必须包含external 和 payable。
当合约接收ETH的时候,receive() 会被触发。receive() 最好不要执行太多的逻辑因为如果别人用 send 和 transfer 方法发送ETH的话,gas会限制在2300,receive() 太复杂可能会触发Out of Gas
报错;如果用 call 就可以自定义 gas 执行更复杂的逻辑(这三种发送ETH的方法我们之后会讲到)。
我们可以在receive()里发送一个event,例如:
// 定义事件
event Received(address Sender, uint Value);
// 接收ETH时释放Received事件
receive() external payable {
emit Received(msg.sender, msg.value);
}
有些恶意合约,会在 receive() 函数(老版本的话,就是 fallback() 函数)嵌入恶意消耗gas的内容或者使得执行故意失败的代码,导致一些包含退款和转账逻辑的合约不能正常工作,因此写包含退款等逻辑的合约时候,一定要注意这种情况。
fallback
fallback() 函数会在调用合约不存在的函数时被触发。可用于接收ETH,也可以用于代理合约proxy contract。fallback()声明时不需要 function 关键字,必须有 external 修饰,一般也会用payable修饰,用于接收ETH:fallback() external payable { ... }
。
当存在calldata的时候,调用的本合约的fallback,而不是目标合约的fallbacl,这点需要注意
我们定义一个fallback()函数,被触发时候会释放fallbackCalled事件,并输出msg.sender,msg.value和msg.data:
// fallback
fallback() external payable {
emit fallbackCalled(msg.sender, msg.value, msg.data);
}
receive和fallback的区别
receive和fallback都能够用于接收ETH,他们触发的规则如下:
简单来说,合约接收ETH时,msg.data为空且存在receive()时,会触发receive();msg.data不为空或不存在receive()时,会触发fallback(),此时fallback()必须为 payable。
receive()和payable fallback()均不存在的时候,向合约直接发送ETH将会报错(你仍可以通过带有payable的函数向合约发送ETH)。
演示示例等我们介绍完发送eth之后再一起看。
发送eth
Solidity有三种方法向其他合约发送ETH,他们是:transfer(),send()和call(),其中call()是被鼓励的用法。
我们先写一个接受eth的合约用来测试,很简单,定义了 receive 和 fallback 函数,一个获取余额的方法以及Log事件用来记录转账的来源地址和转了多少代币
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.17;
contract ReceiveETH {
// 收到eth事件,记录amount和gas
event Log(address from, uint256 amount);
// receive方法,接收eth时被触发
receive() external payable {
emit Log(msg.sender, msg.value);
}
fallback() external payable {
emit Log(msg.sender, msg.value);
}
// 返回合约ETH余额
function getBalance() public view returns (uint256) {
return address(this).balance;
}
}
:::danger
我们将实现三种方法向ReceiveETH合约发送ETH。我们在发送ETH合约SendETH中实现payable的构造函数和receive(),让我们能够在部署时可以给一定数量的初始代币。(下面代码中第6行的payable)
否则合约里面如果没有足够的代币,将会发送失败。
:::
transfer
- 用法是
接收方地址
.transfer(发送ETH数额)。 - transfer()的gas限制是2300,足够用于转账,但对方合约的fallback()或receive()函数不能实现太复杂的逻辑。
- transfer()如果转账失败,会自动revert(回滚交易)。
如果想要给目标合约发送代币,则目标合约的地址必须加上payable修饰符,否则无法转账。
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.17;
contract SendETH {
// 构造函数,payable使得部署的时候可以转eth进去
constructor() payable {}
// receive方法,接收eth时被触发
receive() external payable {}
fallback() external payable {}
// 用transfer()发送ETH
function transferETH(address payable _to, uint256 amount) external { // 目标地址必须加上payable修饰符
_to.transfer(amount);
}
// 返回合约ETH余额
function getBalance() public view returns (uint256) {
return address(this).balance;
}
}
部署的时候我们给1000个代币,此时接收的合约代币为0,发送的合约代币为1000
然后转100个代币给接收合约,通过下图可以看到已经转账成功,并且receive()被触发,并记录了Log
如果我们再转999个代币,此时余额只剩900个了,那么就无法转账,最终revert
send
- 用法是
接收方地址
.send(发送ETH数额)。 - send()的gas限制是2300,足够用于转账,但对方合约的fallback()或receive()函数不能实现太复杂的逻辑。
- send()如果转账失败,不会revert。
- send()的返回值是bool,代表着转账成功或失败,需要额外代码处理一下。
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.17;
contract SendETH {
// 构造函数,payable使得部署的时候可以转eth进去
constructor() payable {}
// receive方法,接收eth时被触发
receive() external payable {}
fallback() external payable {}
// send()发送ETH
function sendETH(address payable _to, uint256 amount) external { // 目标地址必须加上payable修饰符
// 处理下send的返回值,如果失败,revert交易并发送error
bool success = _to.send(amount);
if (!success) {
revert();
}
}
// 返回合约ETH余额
function getBalance() public view returns (uint256) {
return address(this).balance;
}
}
然后转100个代币给接收合约,通过下图可以看到已经转账成功,并且receive()被触发,并记录了Log
如果我们再转999个代币,此时余额只剩700个了(转了3次100),那么就无法转账,最终revert
call(推荐)
- 用法是
接收方地址
.call{value: 发送ETH数额}(“”)。 - call()没有gas限制,可以支持对方合约fallback()或receive()函数实现复杂逻辑。
- call()如果转账失败,不会revert。
- call()的返回值是(bool, data),其中bool代表着转账成功或失败,需要额外代码处理一下。
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.17;
contract SendETH {
// 构造函数,payable使得部署的时候可以转eth进去
constructor() payable {}
// receive方法,接收eth时被触发
receive() external payable {}
fallback() external payable {}
function callETH(address payable _to, uint256 amount) external { // 目标地址必须加上payable修饰符
(bool success, ) = _to.call{value: amount}("");
if (!success) {
revert();
}
}
// 返回合约ETH余额
function getBalance() public view returns (uint256) {
return address(this).balance;
}
}
然后转100个代币给接收合约,通过下图可以看到已经转账成功,并且receive()被触发,并记录了Log
如果我们再转999个代币,此时余额只剩900个了,那么就无法转账,最终revert
转载请注明来源,欢迎对文章中的引用来源进行考证,欢迎指出任何有错误或不够清晰的表达。可以在下面评论区评论,也可以邮件至 289211569@qq.com