Uninitialized Storage Pointers
The EVM stores data either as storage or as memory. Understanding exactly how this is done and the default types for local variables of functions is highly recommended when developing contracts. This is because it is possible to produce vulnerable contracts by inappropriately intializing variables.
To read more about storage and memory in the EVM, see the Solidity documentation on data location, layout of state variables in storage, and layout in memory.
Note | This section is based on an excellent post by Stefan Beyer. Further reading on this topic, inspired by Stefan, can be found in this Reddit thread. |
The Vulnerability
Local variables within functions default to storage or memory depending on their type. Uninitialized local storage variables may contain the value of other storage variables in the contract; this fact can cause unintentional vulnerabilities, or be exploited deliberately.
Let’s consider the relatively simple name registrar contract in NameRegistrar.sol.
Example 12. NameRegistrar.sol
// A locked name registrar
contract NameRegistrar {
bool public unlocked = false; // registrar locked, no name updates
struct NameRecord { // map hashes to addresses
bytes32 name;
address mappedAddress;
}
// records who registered names
mapping(address => NameRecord) public registeredNameRecord;
// resolves hashes to addresses
mapping(bytes32 => address) public resolve;
function register(bytes32 _name, address _mappedAddress) public {
// set up the new NameRecord
NameRecord newRecord;
newRecord.name = _name;
newRecord.mappedAddress = _mappedAddress;
resolve[_name] = _mappedAddress;
registeredNameRecord[msg.sender] = newRecord;
require(unlocked); // only allow registrations if contract is unlocked
}
}
This simple name registrar has only one function. When the contract is unlocked
, it allows anyone to register a name (as a bytes32
hash) and map that name to an address. The registrar is initially locked, and the require
on line 25 prevents register
from adding name records. It seems that the contract is unusable, as there is no way to unlock the registry! There is, however, a vulnerability that allows name registration regardless of the unlocked
variable.
To discuss this vulnerability, first we need to understand how storage works in Solidity. As a high-level overview (without any proper technical detail—we suggest reading the Solidity docs for a proper review), state variables are stored sequentially in slots as they appear in the contract (they can be grouped together but aren’t in this example, so we won’t worry about that). Thus, unlocked
exists in slot[0]
, registeredNameRecord
in slot[1]
, and resolve
in slot[2]
, etc. Each of these slots is 32 bytes in size (there are added complexities with mappings, which we’ll ignore for now). The Boolean unlocked
will look like 0x000…0
(64 0s, excluding the 0x
) for false
or 0x000…1
(63 0s) for true
. As you can see, there is a significant waste of storage in this particular example.
The next piece of the puzzle is that Solidity by default puts complex data types, such as structs, in storage when initializing them as local variables. Therefore, newRecord
on line 18 defaults to storage. The vulnerability is caused by the fact that newRecord
is not initialized. Because it defaults to storage, it is mapped to storage slot[0], which currently contains a pointer to unlocked
. Notice that on lines 19 and 20 we then set newRecord.name
to _name
and newRecord.mappedAddress
to _mappedAddress
; this updates the storage locations of slot[0] and slot[1], which modifies both unlocked
and the storage slot associated with registeredNameRecord
.
This means that unlocked
can be directly modified, simply by the bytes32 _name
parameter of the register
function. Therefore, if the last byte of _name
is nonzero, it will modify the last byte of storage slot[0]
and directly change unlocked
to true
. Such _name
values will cause the require
call on line 25 to succeed, as we have set unlocked
to true
. Try this in Remix. Note the function will pass if you use a _name
of the form:
0x0000000000000000000000000000000000000000000000000000000000000001
Preventative Techniques
The Solidity compiler shows a warning for unintialized storage variables; developers should pay careful attention to these warnings when building smart contracts. The current version of Mist (0.10) doesn’t allow these contracts to be compiled. It is often good practice to explicitly use the memory
or storage
specifiers when dealing with complex types, to ensure they behave as expected.
Real-World Examples: OpenAddressLottery and CryptoRoulette Honey Pots
A honey pot named OpenAddressLottery was deployed that used this uninitialized storage variable quirk to collect ether from some would-be hackers. The contract is rather involved, so we will leave the analysis to the Reddit thread where the attack is quite clearly explained.
Another honey pot, CryptoRoulette, also utilized this trick to try and collect some ether. If you can’t figure out how the attack works, see “An Analysis of a Couple Ethereum Honeypot Contracts” for an overview of this contract and others.