solidity-接收和发送eth

  1. solidity-接收和发送eth
  2. 接收eth
    1. receive
    2. fallback
    3. receive和fallback的区别
  3. 发送eth
    1. transfer
    2. send
    3. call(推荐)

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,他们触发的规则如下:

ebc6e2c7-1312-4c32-a4d0-2c9f2e1390fa-receive_fallback.jpg

简单来说,合约接收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
3bf2181e-2d63-42f2-8acc-ee5191c9f23d-image.png

然后转100个代币给接收合约,通过下图可以看到已经转账成功,并且receive()被触发,并记录了Log
ff577069-2207-4ebf-bbf1-1d6b7c6a8a04-image.png

如果我们再转999个代币,此时余额只剩900个了,那么就无法转账,最终revert
c8595195-3d31-4ebe-a6c5-85101479d4bf-image.png

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
92d57913-d72d-4e2e-839d-431f37daa46c-image.png

如果我们再转999个代币,此时余额只剩700个了(转了3次100),那么就无法转账,最终revert
f12270b2-c253-424f-ac26-33474a062e04-image.png

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
75d3374a-27ce-4aca-a73e-fe1fdd549743-image.png

如果我们再转999个代币,此时余额只剩900个了,那么就无法转账,最终revert
6f44c496-e4fb-4f5a-ba9a-3235aaa45b6c-image.png


转载请注明来源,欢迎对文章中的引用来源进行考证,欢迎指出任何有错误或不够清晰的表达。可以在下面评论区评论,也可以邮件至 289211569@qq.com