Default Visibilities
Functions in Solidity have visibility specifiers that dictate how they can be called. The visibility determines whether a function can be called externally by users, by other derived contracts, only internally, or only externally. There are four visibility specifiers, which are described in detail in the Solidity docs. Functions default to public
, allowing users to call them externally. We shall now see how incorrect use of visibility specifiers can lead to some devastating vulnerabilities in smart contracts.
The Vulnerability
The default visibility for functions is public
, so functions that do not specify their visibility will be callable by external users. The issue arises when developers mistakenly omit visibility specifiers on functions that should be private (or only callable within the contract itself).
Let’s quickly explore a trivial example:
contract HashForEther {
function withdrawWinnings() {
// Winner if the last 8 hex characters of the address are 0
require(uint32(msg.sender) == 0);
_sendWinnings();
}
function _sendWinnings() {
msg.sender.transfer(this.balance);
}
}
This simple contract is designed to act as an address-guessing bounty game. To win the balance of the contract, a user must generate an Ethereum address whose last 8 hex characters are 0. Once achieved, they can call the withdrawWinnings
function to obtain their bounty.
Unfortunately, the visibility of the functions has not been specified. In particular, the _sendWinnings
function is public
(the default), and thus any address can call this function to steal the bounty.
Preventative Techniques
It is good practice to always specify the visibility of all functions in a contract, even if they are intentionally public
. Recent versions of solc show a warning for functions that have no explicit visibility set, to encourage this practice.
Real-World Example: Parity Multisig Wallet (First Hack)
In the first Parity multisig hack, about $31M worth of Ether was stolen, mostly from three wallets. A good recap of exactly how this was done is given by Haseeb Qureshi.
Essentially, the multisig wallet is constructed from a base Wallet
contract, which calls a library contract containing the core functionality (as described in Real-World Example: Parity Multisig Wallet (Second Hack)). The library contract contains the code to initialize the wallet, as can be seen from the following snippet:
contract WalletLibrary is WalletEvents {
...
// METHODS
...
// constructor is given number of sigs required to do protected
// "onlymanyowners" transactions as well as the selection of addresses
// capable of confirming them
function initMultiowned(address[] _owners, uint _required) {
m_numOwners = _owners.length + 1;
m_owners[1] = uint(msg.sender);
m_ownerIndex[uint(msg.sender)] = 1;
for (uint i = 0; i < _owners.length; ++i)
{
m_owners[2 + i] = uint(_owners[i]);
m_ownerIndex[uint(_owners[i])] = 2 + i;
}
m_required = _required;
}
...
// constructor - just pass on the owner array to multiowned and
// the limit to daylimit
function initWallet(address[] _owners, uint _required, uint _daylimit) {
initDaylimit(_daylimit);
initMultiowned(_owners, _required);
}
}
Note that neither of the functions specifies their visibility, so both default to public
. The initWallet
function is called in the wallet’s constructor, and sets the owners for the multisig wallet as can be seen in the initMultiowned
function. Because these functions were accidentally left public
, an attacker was able to call these functions on deployed contracts, resetting the ownership to the attacker’s address. Being the owner, the attacker then drained the wallets of all their ether.