在 0.6.x 之前的 Solidity 版本中,开发人员通常使用回退函数 来处理两种情况下的逻辑
- 合约收到以太坊而没有数据
- 合约收到数据,但没有函数与被调用的函数匹配
0.6.x 之前版本的回退函数的主要用例是接收以太坊并对其做出反应,这是令牌式合约中常用的模式,用于拒绝转账、发出事件或转发以太坊。当没有数据调用合约时,例如通过.send() 或 .transfer() 函数,该函数就会执行。0.5.x 语法是
pragma solidity ^0.5.0; contract Charity { function() external payable { // React to receiving ether } }
第二个用例因“委托代理”模式而变得流行,该模式用于实现可升级合约。它有一个简单的代理合约,只声明一个回退函数。当合约中的任何函数都不匹配调用数据中的函数标识符时,就会调用回退函数。这允许“委托代理”模式,其中功能在被调用合约之外实现。示例实现
pragma solidity ^0.5.0; contract DelegateProxy { address internal implementation; function() external payable { address addr = implementation; assembly { calldatacopy(0, 0, calldatasize()) let result := delegatecall(gas(), addr, 0, calldatasize(), 0, 0) returndatacopy(0, 0, returndatasize()) switch result case 0 { revert(0, returndatasize()) } default { return(0, returndatasize()) } } } }
调用合约使用汇编代码,我们在此不做详细介绍,但您可以在 Zeppelin 的文档 中了解更多信息。
拆分回退函数
我们意识到该函数的双重用途会让开发人员感到困惑,这会导致潜在的安全问题。例如,开发人员通常会实现回退函数,预期只有以太坊转账才会调用它,但它也会在合约中缺少函数时被调用。令人困惑的是,由于这是预期行为,因此没有报告错误。以下是演示此令人困惑行为的示例实现。
pragma solidity ^0.5.0; contract Charity { mapping (address => uint256) public donations; function processDonation(address user) external payable { donations[user] += msg.value; } } contract Receiver { event ValueReceived(address user, uint amount); function() external payable { emit ValueReceived(msg.sender, msg.value); } } contract CharitySplitter { function donate(Charity charity) external payable { charity.processDonation.value(msg.value)(msg.sender); } }
当用慈善合约地址调用 CharitySplitter.donate() 时,其 processDonation 函数会按预期正确调用以处理捐赠。但是,如果错误地传递了 Receiver 合约地址,则会调用其回退函数,从而吞并发送的值。
const goodCharity = await Charity.new(); const receiver = await Receiver.new(); const badCharity = await Charity.at(receiver.address); const charitySplitter = await CharitySplitter.new(); // Charity.processDonation is executed successfully and 10 wei donation is recorded await charitySplitter.donate(goodCharity, { value: 10 }); // Triggers the underlying Receiver fallback function // 10 wei is acquired and ValueReceived event emitted await charitySplitter.donate(badCharity, { value: 10 });
由于 EVM 是无类型的,Solidity 无法从地址检查合约的实际类型,而必须依赖用户提供的类型。函数签名也不能完全解决类型混淆问题,但可以在许多情况下起作用。
这就是为什么在 0.6.x 版本中,回退函数被拆分为两个独立的函数的原因
- receive() external payable - 用于空 calldata(和任何值)
- fallback() external payable - 当没有其他函数匹配(甚至没有接收函数)时。可选地 payable。
这种分离为希望接收纯以太坊的合约提供了回退函数的替代方案。
receive()
合约现在只能有一个 receive 函数,声明语法为:receive() external payable {…}(没有 function 关键字)。
它在对合约的无数据调用(calldata)上执行,例如通过 send() 或 transfer() 进行的调用。
该函数不能有任何参数,不能返回任何内容,并且必须具有 external 可见性和 payable 状态可变性。要复制 0.6.0 下面的示例,请使用以下代码
pragma solidity ^0.5.0; contract Charity { receive() external payable { // React to receiving ether } }
fallback()
回退函数现在有不同的语法,使用 fallback() external [payable] {…}(没有 function 关键字)声明。该函数不能有任何参数,不能返回任何内容,并且必须具有 external 可见性。回退函数总是接收数据,但为了也接收以太坊,您应该将其标记为 payable。要复制 0.6.0 下面的示例,请使用以下代码
pragma solidity ^0.6.0; contract DelegateProxy { address internal implementation; fallback() external payable { address addr = implementation; assembly { calldatacopy(0, 0, calldatasize()) let result := delegatecall(gas(), addr, 0, calldatasize(), 0, 0) returndatacopy(0, 0, returndatasize()) switch result case 0 { revert(0, returndatasize()) } default { return(0, returndatasize()) } } } }
迁移和修复示例合约
因此,我们将问题合约转换为 v0.6.x 版本,让它声明一个 receive() 函数,该函数只接受没有数据的传入以太坊,并避免了上面演示的导致价值损失的类型混淆。
pragma solidity ^0.6.0; contract Charity { mapping (address => uint256) public donations; function processDonation(address user) external payable { donations[user] += msg.value; } } contract Receiver { event ValueReceived(address user, uint amount); receive() external payable { emit ValueReceived(msg.sender, msg.value); } } contract CharitySplitter { function donate(Charity charity) external payable { charity.processDonation{value:msg.value}(msg.sender); } }
当用 Receiver 合约地址调用修复后的合约时,调用现在将回滚
// The following call now reverts await charitySplitter.donate(badCharity, { value: 10 });
我们希望您发现回退函数的逻辑划分对您的设计更加清晰,并欢迎您对新语法提出任何反馈。