Storage Allocation Exploits in Ethereum Smart Contracts

Edit: Updated the “Defrauding de Fraudster” section to reflect the correction provided by Sven Stucki.

Introduction

In a previous article we introduced some common Ethereum security vulnerabilities, which every Ethereum developer should be aware of.

Inspired by a discussion on Reddit, in this article we look at slightly more complex attack, which exploits the way Solidity deals with storage allocations.

Code Example

Let’s consider the following code:

The contract seems fairly self-explanatory. Its purpose is to allow users to pay in some Ether which will be locked during a specified time interval. This is known as hodl contract, aimed at avoiding the temptation to sell your Ether investment in times of volatility. You are essentially forced to hold until the unlock time has passed.

The Exploit

There is however a trick to the way the payIn() function works, which causes the money paid in being allocated to the owner of the contract, instead of the person making the deposit.

As you can see from the code, the information on balance is stored in a struct, which is used in a balance mapping:

struct HoldRecord {
   uint amount;
   uint unlockTime;
}

This struct is used in the payIn() function, however it is declared and used as follows:

HoldRecord newRecord;
newRecord.amount += msg.value;
newRecord.unlockTime = now + holdTime;
balance[msg.sender] = newRecord;

Because this declaration is done within a function, and because structs in Solidity default to storage, newRecord is actually a pointer to a struct in storage. However, this pointer is uninitialised and points to address 0.

The way storage is allocated, at address 0 resides the first variable declared in the contract. In our case we start with the following two variable declarations:

uint ownerAmount;
uint numberOfPayouts;

This means that the payIn() function actually adds the deposited funds to ownerAmount and also overwrites the fairly useless numberOfPayouts variable, which is just there as a buffer to avoid overwriting the owner address.

The owner can now use ownerWithdrawal() to steal the funds.

Defrauding the Fraudster

There is more to this example however. The attacker who has written this code has made a slight error:

The following line in payIn() copies the amount storage at storage address 0 (the owner’s share) to the caller’s balance:

balance[msg.sender] = newRecord;

This means, the clever counter-attacker can actually make a small deposit with a very short lock time and immediately call withdraw() to receive all funds allocated to the owner, before the owner can make use of his exploit.

Conclusion

The point of this example is to highlight the danger of the way storage is handled on the Ethereum blockchain and how the Solidity language deals with this.

The issue arrises in this case by deliberately omitting the storage or memory keywords and thereby forcing Solidity to create an uninitialised pointer.

The Solidity compiler actually warns us about uninitialised storage pointers, which at least makes it harder to introduce such a bug accidentally. However, the victim of a deployed malicious contract never gets to see this warning.

All this highlights the fact that smart contracts on the blockchain are difficult to get right with the existing ecosystems and tools. Until safer options are available we must be very cautious.