{ 跳至内容 }

Solidity 0.6.x 功能:回退函数和接收函数

发布于 2020 年 3 月 26 日,作者 Elena Gesheva

解释器

在 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 });

我们希望您发现回退函数的逻辑划分对您的设计更加清晰,并欢迎您对新语法提出任何反馈。

上一篇

下一篇

参与进来

GitHub

Twitter

Mastodon

矩阵

发现更多

博客文档用例贡献关于论坛

2024Solidity 团队

安全策略

行为准则