为什么在这个智能合约上使用断言?
Why assertion is used on this Smart Contract?
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;
}
}
对于上面的合约,在什么情况下断言失败/address(this).balance 没有减少(msg.value / 2)?为什么我们在这里需要断言?
如果 addr.transfer(msg.value / 2)
失败,它会恢复 sendHalf()
的执行。
因此 assert()
在这种情况下是多余的。
断言是真的,这正是它在那里的原因。您使用 assert()
来声明您认为永远有效的东西。如果结果证明它们是假的,则说明您的合同中存在错误。
断言不仅仅是幻想if
。虽然它确实执行运行时检查,但它也是为形式验证提供目标的方法之一。 Solidity 编译器中内置的 SMTChecker 等工具可以通过尝试证明关于您的代码的各种陈述来检测错误。问题是 - 这样的工具如何判断您得到的结果不是您想要的结果?用断言记录你的假设是一种非常简单的方法,可以为工具提供额外的信息,以从错误的行为中辨别预期的行为。
此外,虽然合约现在很简单而且很容易看出它不会失败,但代码不会永远保持简单。该条件仅在合同没有其他应付功能的假设下成立。每次添加支付功能时,你会记得修改这个功能吗?如果合同增长并且函数被埋在其他几个函数下的文件底部怎么办?最重要的是——以后其他人修改代码怎么办?他们会注意到这个限制吗?断言是一种好方法,它不必依赖任何注意到这一点的人并将其变成自动检查。
最后,断言是正确的,但它显而易见吗?实际上有很多假设:
- 合约只能通过几种特定方式接收以太币:
- 通过调用它的 payable 函数 -
sendHalf()
是这里唯一的一个
- 调用其
receive()
或fallback()
函数-有none
- 成为另一份合同
selfdestruct
的接受者
- 作为区块中开采的以太币的接收者
transfer()
的被叫方无法回调sendHalf()
因为transfer()
转发只有2300gas,外部调用成本更高
transfer()
的被调用者无法执行 selfdestruct
因为它花费了 5000 gas。
transfer()
内的恢复不会以任何方式被沉默,所以即使 selfdestruct
的成本将未来更改为 <= 2300 gas,发出它也会终止执行。
- 以太坊上的交易仅按顺序执行,开采的以太币不能在合同执行的中间转移。
这里有足够的假设,代码的作者可能根本没有 100% 确定他没有遗漏一些可能会变成安全漏洞的模糊角落案例。断言可以是明确排除这种可能性的简单而有效的方法。
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;
}
}
对于上面的合约,在什么情况下断言失败/address(this).balance 没有减少(msg.value / 2)?为什么我们在这里需要断言?
如果 addr.transfer(msg.value / 2)
失败,它会恢复 sendHalf()
的执行。
因此 assert()
在这种情况下是多余的。
断言是真的,这正是它在那里的原因。您使用 assert()
来声明您认为永远有效的东西。如果结果证明它们是假的,则说明您的合同中存在错误。
断言不仅仅是幻想if
。虽然它确实执行运行时检查,但它也是为形式验证提供目标的方法之一。 Solidity 编译器中内置的 SMTChecker 等工具可以通过尝试证明关于您的代码的各种陈述来检测错误。问题是 - 这样的工具如何判断您得到的结果不是您想要的结果?用断言记录你的假设是一种非常简单的方法,可以为工具提供额外的信息,以从错误的行为中辨别预期的行为。
此外,虽然合约现在很简单而且很容易看出它不会失败,但代码不会永远保持简单。该条件仅在合同没有其他应付功能的假设下成立。每次添加支付功能时,你会记得修改这个功能吗?如果合同增长并且函数被埋在其他几个函数下的文件底部怎么办?最重要的是——以后其他人修改代码怎么办?他们会注意到这个限制吗?断言是一种好方法,它不必依赖任何注意到这一点的人并将其变成自动检查。
最后,断言是正确的,但它显而易见吗?实际上有很多假设:
- 合约只能通过几种特定方式接收以太币:
- 通过调用它的 payable 函数 -
sendHalf()
是这里唯一的一个 - 调用其
receive()
或fallback()
函数-有none - 成为另一份合同
selfdestruct
的接受者 - 作为区块中开采的以太币的接收者
- 通过调用它的 payable 函数 -
transfer()
的被叫方无法回调sendHalf()
因为transfer()
转发只有2300gas,外部调用成本更高transfer()
的被调用者无法执行selfdestruct
因为它花费了 5000 gas。transfer()
内的恢复不会以任何方式被沉默,所以即使selfdestruct
的成本将未来更改为 <= 2300 gas,发出它也会终止执行。- 以太坊上的交易仅按顺序执行,开采的以太币不能在合同执行的中间转移。
这里有足够的假设,代码的作者可能根本没有 100% 确定他没有遗漏一些可能会变成安全漏洞的模糊角落案例。断言可以是明确排除这种可能性的简单而有效的方法。