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

  1. // A locked name registrar
  2. contract NameRegistrar {
  3. bool public unlocked = false; // registrar locked, no name updates
  4. struct NameRecord { // map hashes to addresses
  5. bytes32 name;
  6. address mappedAddress;
  7. }
  8. // records who registered names
  9. mapping(address => NameRecord) public registeredNameRecord;
  10. // resolves hashes to addresses
  11. mapping(bytes32 => address) public resolve;
  12. function register(bytes32 _name, address _mappedAddress) public {
  13. // set up the new NameRecord
  14. NameRecord newRecord;
  15. newRecord.name = _name;
  16. newRecord.mappedAddress = _mappedAddress;
  17. resolve[_name] = _mappedAddress;
  18. registeredNameRecord[msg.sender] = newRecord;
  19. require(unlocked); // only allow registrations if contract is unlocked
  20. }
  21. }

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:

  1. 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.