漏洞原理
在 Solidity 中,函数有可见性类型,指示函数被允许如何被调用。可见性决定了函数是否可以由用户、其他派生合约、仅在内部或外部调用。函数有四个可见性类型(关于函数的可见性详见另一篇博文 Solidity语言详解——函数和状态变量的可见性及Getter函数)。Solidity 中的函数默认为公共的,即允许用户或合约从外部调用它们。不正确地使用函数可见性可能会导致智能合约中的一些破坏性漏洞。
安全隐患
Solidity 中函数的默认可见性是
public
。因此,没有指定任何可见性的函数将被外部用户或合约调用。当开发人员错误地忽略了应该是私有 (或仅在合约内部可调用) 的函数的可见性说明时,问题就出现了。
让我们来看一下这个合约例子:
// SPDX-License-Identifier: MIT
pragma solidity ^0.4.18;
contract HashForEther {
function withdrawWinnings() {
// Winner if the last 8 hex characters of the address are 0.
require(uint32(msg.sender) == 0);
withdraw();
}
function withdraw() {
msg.sender.transfer(this.balance);
}
}
这个简单的合约被设计成一个地址猜测赏金游戏。为了赢得合约的余额,用户必须生成一个最后 8 个十六进制字符为 0 的以太坊地址。一旦获得,它们可以调用
withdraw()
函数来获得奖励。
不幸的是,合约中函数的可见性没有被指定。特别是,
withdraw()
函数是公共的,因此任何合约都可以调用该函数来窃取奖金。
如何预防
最好总是在合约中指定所有函数的可见性,即使它们是有意公开的。Solidity 的最新版本现在会在编译过程中对没有显式指定可见性的函数显示警告或错误提示,这样就可以预防此类错误的发生。
以下是经修复后的合约代码,通过将
withdraw()
的可见性设置为
private
来限制该函数仅允许在合约内部调用。
// SPDX-License-Identifier: MIT
pragma solidity ^0.4.18;
contract HashForEtherFixed {
function withdrawWinnings() public {
// Winner if the last 8 hex characters of the address are 0.
require(uint32(msg.sender) == 0);
withdraw();
}
function withdraw() private {
msg.sender.transfer(this.balance);
}
}