为什么在这个智能合约上使用断言?

Cen*_*ngo 4 assert solidity evm

contract Sharer {
    function sendHalf(address payable addr) public payable returns (uint balance) {
        require(msg.value % 2 == 0, "Even value required.");
        uint balanceBeforeTransfer = address(this).balance;
        addr.transfer(msg.value / 2);
        // Since transfer throws an exception on failure and
        // cannot call back here, there should be no way for us to
        // still have half of the money.
        assert(address(this).balance == balanceBeforeTransfer - msg.value / 2);
        return address(this).balance;
    }
}
Run Code Online (Sandbox Code Playgroud)

对于上述合约,在什么情况下断言失败/address(this).balance 不会减少 (msg.value / 2)?为什么我们需要在这里断言?

cam*_*eel 5

这个断言是正确的,这正是它存在的原因。你用来assert()声明你认为永远有效的事情。如果它们被证明是错误的,那么你的合同中就有错误。

断言不仅仅是一种幻想if。虽然它确实执行运行时检查,但它也是提供形式验证目标的方法之一。Solidity 编译器中内置的SMTChecker等工具可以通过尝试证明有关代码的各种陈述来检测错误。问题是 - 这样的工具如何判断你得到的结果不是你想要的结果?用断言记录你的假设是一种非常简单的方法,可以为工具提供区分预期行为和有缺陷行为所需的额外信息。

此外,虽然现在合约很简单并且很容易看出它不会失败,但代码不会永远保持简单。该条件仅在合同没有其他应付功能的假设下成立。每次添加付费功能时你会记得修改这个功能吗?如果合约增长并且该函数被埋在文件底部的几个其他函数下怎么办?最重要的是——其他人将来修改代码怎么办?他们会注意到这个限制吗?断言是一种好方法,不必依赖任何人注意到这一点并将其转变为自动检查。

最后,这个断言是正确的,但它是显而易见的吗?实际上有很多假设:

  • 合约只能通过几种特定方式接收以太币:
    1. 通过调用其应付函数 -sendHalf()是这里唯一的一个
    2. 调用其receive()fallback()函数 - 没有
    3. selfdestruct成为另一份合同的接收者
    4. 成为区块中开采的以太币的接收者
  • 被叫方transfer()无法回调sendHalf(),因为transfer()仅转发 2300 Gas,外部调用成本更高。
  • 的被调用者transfer()无法执行,selfdestruct因为它花费了 5000 Gas。
  • 内部的恢复transfer()不会以任何方式沉默,所以即使selfdestruct改变未来的成本 <= 2300 Gas,发出它无论如何都会终止执行。
  • 以太坊上的交易只能按顺序执行,开采的以太币不能在合约执行过程中转移。

这里有足够的假设,代码的作者可能根本无法 100% 确定他没有错过一些可能会变成安全漏洞的晦涩的极端情况。断言可以是明确排除这种可能性的简单而有效的方法。