美文网首页Solidity智能合约专题
Solidity的发账和收账详解

Solidity的发账和收账详解

作者: 梁帆 | 来源:发表于2022-11-10 00:36 被阅读0次

1.发账

发账有三种方式:transfer, sendcall

(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()进行收账,本质上跟直接函数调用差不多,这不是我们说的重点,主要的重点在与receivefallback

关于receive和fallback,我们在《Solidity的fallback和receive》聊得比较详细,至于这两者的选择和优先级的问题,有以下的调用规则:

call方法转账的调用规则
在下面的示例中,我们会详细探讨这个问题。

(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

相关文章

网友评论

    本文标题:Solidity的发账和收账详解

    本文链接:https://www.haomeiwen.com/subject/vqoatdtx.html