目前来看,智能合约接受以太共有五种可能性;
1. receive()
一个合约最多有一个 receive
函数, 声明函数为: receive() external payable {}
无需 function
关键字,也没有参数和返回值并且必须是external
可见性和payable
修饰。 它可以是 virtual
的,可以被重载也可以有修改器modifier 。
在对合约没有任何附加数据调用(通常是对合约转账)是会执行 receive
函数。
例如通过 .send()
or .transfer()
如果 receive
函数不存在,但是有payable的 fallback 回退函数 那么在进行纯以太转账时,fallback 函数会被调用。
如果两个函数都没有,这个合约就没法通过常规的转账交易接收以太,会抛出异常。
并且,receive
函数只有 2300 gas 可以使用, 除了基础的日志输出之外,进行其他操作的余地很小。下面的操作消耗会操作 2300 gas :
- 写入存储
- 创建合约
- 调用消耗大量 gas 的外部函数
- 发送以太币
不过,与任何其他函数一样,只要有足够的 gas 传递给它,回退函数就可以执行复杂的操作。
pragma solidity ^0.6.0;
// 这个合约会保留所有发送给它的以太币,没有办法取回。
contract Sink {
event Received(address, uint);
receive() external payable {
emit Received(msg.sender, msg.value);
}
}
2. 实现payable的fallback()
合约可以最多有一个回退函数。函数声明为: fallback() external [payable]
或 fallback() (bytes calldata input) external [payable] returns (bytes memory output)
没有function
关键字。必须是external
可见性,它可以是 virtual
的,可以被重载也可以有 修改器modifier 。
fallback 函数始终会接收数据,但为了同时接收以太时,必须标记为payable
。
如果使用了带参数的版本, input
将包含发送到合约的完整数据(等于 msg.data
),并且通过 output
返回数据。 返回数据不是 ABI 编码过的数据,相反,它返回不经过修改的数据。
如果回退函数在接收以太时调用,只有 2300 gas 可以使用。
不过,与任何其他函数一样,只要有足够的 gas 传递给它,回退函数就可以执行复杂的操作。
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.6.2 <0.9.0;
contract Test {
// 发送到这个合约的所有消息都会调用此函数(因为该合约没有其它函数)。
// 向这个合约发送以太币会导致异常,因为 fallback 函数没有 `payable` 修饰符
fallback() external { x = 1; }
uint x;
}
// 这个合约会保留所有发送给它的以太币,没有办法返还。
contract TestPayable {
uint x;
uint y;
// 除了纯转账外,所有的调用都会调用这个函数.
// (因为除了 receive 函数外,没有其他的函数).
// 任何对合约非空calldata 调用会执行回退函数(即使是调用函数附加以太).
fallback() external payable { x = 1; y = msg.value; }
// 纯转账调用这个函数,例如对每个空empty calldata的调用
receive() external payable { x = 2; y = msg.value; }
}
contract Caller {
function callTest(Test test) public returns (bool) {
(bool success,) = address(test).call(abi.encodeWithSignature("nonExistingFunction()"));
require(success);
// test.x 结果变成 == 1。
// address(test) 不允许直接调用 ``send`` , 因为 ``test`` 没有 payable 回退函数
// 转化为 ``address payable`` 类型 , 然后才可以调用 ``send``
address payable testPayable = payable(address(test));
// 以下将不会编译,但如果有人向该合约发送以太币,交易将失败并拒绝以太币。
// test.send(2 ether);
}
function callTestPayable(TestPayable test) public returns (bool) {
(bool success,) = address(test).call(abi.encodeWithSignature("nonExistingFunction()"));
require(success);
// 结果 test.x 为 1 test.y 为 0.
(success,) = address(test).call{value: 1}(abi.encodeWithSignature("nonExistingFunction()"));
require(success);
// 结果test.x 为1 而 test.y 为 1.
// 发送以太币, TestPayable 的 receive 函数被调用.
// 因为函数有存储写入, 会比简单的使用 ``send`` or ``transfer``消耗更多的 gas。
// 因此使用底层的call调用
(success,) = address(test).call{value: 2 ether}("");
require(success);
// 结果 test.x 为 2 而 test.y 为 2 ether.
return true;
}
}
注:实现payable的fallback() 和receive() 的区别
- receive()优先接受纯以太的交易
- 实现payable的fallback()优先接受附带msg.data的交易
3. 实现payable的函数
这种方式无需多言,加个payable关键词就可以了!
4. selfdestruct()
自毁函数是具有攻击性的一种让其它合约被迫接受以太的方式
contract Attack{
address private owner;
constructor(){
owner = msg.sender;
}
event Received(address, uint);
receive() external payable {
emit Received(msg.sender, msg.value);
}
//自毁转账
function selfdestructAttack(address _to) external public{
require(msg.sender == this.owner,"you are not the owner");
selfdestruct(_to);
}
}
如上例所示,当调用合约的selfdestructAttack
函数时,此合约将会自毁,并且将合约内的余额强制发送到 _ to这个合约地址上,无论_ to合约是否实现接受以太的函数,都不得不接受这笔转账。
5. miner区块奖励
我们常说的挖矿奖励,当挖出了一个新区块时,以太奖励将会达到挖出人指定的地址上,无论这个地址是什么,它都会多出这笔奖励余额;
如今以太坊转型为PoS权益证明机制,miner区块奖励将会逐渐销声匿迹。