2
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;
    }
}

对于上面的合约,在什么情况下断言失败/地址(this).balance 没有减少(msg.value / 2)?为什么我们在这里需要断言?

4

2 回答 2

2

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

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

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

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

  • 合约只能通过几种特定方式接收以太币:
    1. 通过调用其支付功能 -sendHalf()是这里唯一的一个
    2. 调用它的receive()orfallback()函数 - 没有
    3. selfdestruct成为另一份合同的接受者
    4. 成为区块中开采的以太币的接收者
  • 的被调用者transfer()无法回拨,sendHalf()因为transfer()只转发 2300 gas,而外部调用成本更高。
  • 的被调用者transfer()无法执行selfdestruct,因为它花费了 5000 gas。
  • 内部transfer()的还原不会以任何方式静音,因此即使selfdestruct将 future 的成本更改为 <= 2300 gas,发布它也会终止执行。
  • 以太坊上的交易只能按顺序执行,开采的以太币不能在合约执行过程中转移。

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

于 2021-12-04T23:57:18.877 回答
0

如果addr.transfer(msg.value / 2)失败,它会恢复sendHalf().

所以assert()在这种情况下是多余的。

于 2021-12-03T22:48:05.620 回答