Hacking Secrets in Ethereum Smart Contracts

S. Alexander Zaman
Coinmonks
10 min readAug 22, 2022

--

A demonstration of why you can’t store secrets in “private” methods.

When I was starting to learn about solidity and smart contracts, one simple contract I tried to make when I first tested it out was a method that would allow withdrawals if the user provided the right password.

It seemed like a simple enough app to make and I figured I could just store the password as a private variable in the smart contract, that I could even change in the future with a changePassword method.

string private password = "CanIHazMyMoniesPlz";

There are many applications where storing secrets would have been nice.
For example, I could:

  • create a Wordle game where people paid ETH per guess
  • hide URLs for secret content to be visible only by token holders
  • store a random seed for a random number generator in a web3 game
  • make an NFT whose features reveal after a certain timestamp
  • etc.

However, very quickly after I looked more into this, I learned that this was not at all a secure way of handling private information. Solidity contracts are stored on the blockchain and the blockchain is publically accessible to the world. Any data in your contracts can be retrieved and exploited by an ambitious blockchain explorer.

Do not store secrets in your smart contracts!

Despite the name, data stored in “private” variables are visible to everyone.

Why Are they called “private” if they’re not?

When learning solidity, you learn that you can create variables with the following scope (from least to most permissive):

  • private, internal, external, public

When trying to access one smart contract from another, the calling smart contract cannot see private variables. Because of this, one might think that you could safely store secret information in private variables and not have to worry about people accessing and exploiting the private information.

However, this declaration does not mean “private” in the sense that nobody can read the data. Private, instead, refers to the scope of the variable (i.e., what code can access it).

These are more clarifying definitions of the keywords mentioned above:

  • private: Can only be CALLED from its own contract
  • internal: can be CALLED from itself and derived contracts
  • external: can be CALLED from external contracts (all contracts that are not internal)
  • public: can be CALLED from both internal and external contracts

Scope keywords do not affect visibility of variables because variables are stored in the blockchain and always by everyone.

A Demonstration from scratch

Photo by Mark Williams on Unsplash

Below I’ll show you a step-by-step demonstration of this concept in play.

We will do the following:

  1. Set up hardhat for a development environment
  2. Create and compile the smart contract and deployment script
  3. Setup a local ethereum node and deploy the smart contract locally
  4. Run some code to retrieve the value of a private variable in the smart contract

Bust open your terminal and lets begin!

Setup Hardhat

Hardhat is a popular tool used for developing solidity contracts on Ethereum. We’re going to use it for our demonstration so we will first need to install it.

I am assuming that you already have npm installed. If you do not please install it following a guide such as this one:

First let’s set up a hardhat project

  1. create a new folder
  2. go into the folder and run npx hardhat

3. We’re going to use javascript for this project just to keep the code simple. Select that option and press enter to accept the rest of the default selections.

You should see a screen similar to the below.

It should then begin installing the npm dependencies and set up your project. Once it’s complete you can check the directory and you should see something like the following…

4. Open ./contracts/Lock.sol in your favorite editor (I will be using VSCode).

Setup a smart contract with a secret

The contract you opened locks ETH for a specified duration of time. We can modify this smart contract to have a method that lets you grab the funds early if you provide the right password stored as a private variable.

  1. Create a new private method called as follows:
    string private password;
  2. Update the constructor to set the password:

3. Create a new forceWithdrawmethod that is the same as withdraw but takes a password and checks that instead of the timestamp. It should look like this:

(Note that Solidity can’t compare strings so I had to add a helper function strcmpto perform a string equality check)

Your whole contract should look like the following:

4. Finally you need to update your deploy settings to set the password. You just need to add the password parameter to the deploy method like so:
const lock = await Lock.deploy(unlockTime, "CanIHazMyMoniesPlz", {value: lockedAmount});

I refactored the deploy script to extract the deployment into a private method but you can see how mine ended up below:

Albeit contrived, this contract will allow the owner to retrieve the locked ETH early by entering the password. The password was set up with the contract was created but is not visible in the contract code itself.

As you’ll see in the next section, even if you don’t have access to the deploy script, you’ll still be able to figure out what’s stored in the private password parameter

Running a local Ethereum node and deploying

Now that you have your code ready you can start a local instance of ethereum and deploy to that blockchain. We can do this quite easily with hardhat commands:

  1. Run npx hardhat node to run your node locally
    - Note that it will produce a bunch of wallets with addresses and private keys.
    - Also note the address it runs on at the top. Most likely it should be running on http://localhost:8545. The message would look like the following:

2. In another terminal window, compile and deploy your script to the local node with the following command:

You can see that it gets deployed by checking your ethereum node terminal window you should see something similar to the following:

3. Take note of the contract address here. If you go back to your other window you should also see the contract address in the response to running your command. it should look something like this:

You’ve now deployed the contract locally!

Next, we’ll go into the console to access the blockchain and figure out the password!

Extracting the secret from the blockchain

Now comes the fun part! let’s go into a javascript console and retrieve the storage information.

  1. Hardhat comes with a javascript console that provides some key functionality as well as automatically loads up the library ethers.js. We’ll use this library to make network calls to the local Ethereum blockchain. Load the console, and attach it to the blockchain on localhost with the following command:

2. Next we’ll initialize a provider with the following command:

Think of a provider as a client interface to the blockchain. This can be used to get the information we need from our blockchain node.

3. Let’s use the provider to check the current balance of the owner's wallet. We can compare this to prove that forceWithdraw actually worked.
Run the following command and remember the number returned (it should be something like 9998.99…):

4. Now let’s connect to the contract. Do so with the following code but make sure to replace the address being attached with the address of your contract:

5. You can test out that forceWithdraw doesn’t work by trying it with the wrong password. Try something like the following:

As you can see the command doesn’t work without the right password.

Let’s check the node storage for the right password

6. In the hardhat console, run the following command

NOTE: The 2 represents the memory location. I’ll explain that in the next section

you can check the value returned for passwordHex, it should look something like the below:

7. A hex value is not all that helpful for us. We now have to convert the hex to a string. Let’s make a function that converts it to a readable string

8. Run the function against the passwordHex to get the value

Safe Cracked!

Now, you can use the password you recovered to retrieve your funds:

What’s this Mysterious ‘2' location?

One thing I glossed over above is how we got the location parameter ‘2’ from the getStorageAt function.

To get that value, we have to understand how storage works.

Here’s a simplistic summary of the rules for Solidity:

  • Variables that are declared outside of a function are state variables and all state variables are stored in storage by default.
  • immutable and constant are actually compiled into the code and would thus not occupy any storage.
  • Each storage slot has a size of 32 bytes
  • A single storage slot can hold multiple variables if they fit
    - e.g., a boolean only needs a bit and so you could store 8 * 32 = 256 booleans in a single slot if they’re back to back
  • struct and fixed arrays start a new storage slot
  • Dynamic arrays create a slot per element
  • maps are a concatenation of the key and the storage slot of where the map was declared [look at this SO answer for more details on how maps are stored]

This is a bit simplified but enough for you to follow along. If you want more details on how the EVM handles storage, check this guide.

Using these rules, we can look at our solidity code and figure out that the password is stored in slot 2 as you can see below:

Don’t Keep Secrets in Storage

As demonstrated above, secrets stored in storage are not safe. What’s worse is that hackers could probably create tools to simplify or automate a lot of the steps, I suggested above to make this process significantly faster and easier.

Your best bet is to not rely on these secrets to exist in the first place. However, sometimes you’ll need to store secrets for your application. Finding options to handle this is beyond the scope of this article but here are some things you can explore:

  1. Does that part of your application need to belong on the blockchain? The blockchain is very good for many use cases and not so good for others. Consider if there’s a better alternative, such as making calls to regular web services. This creates issues with centralization but might be relevant in certain situations.
  2. can you rely on msg.sender. Msg.sender is a field that can’t really be manipulated without owning the key. In certain applications, maybe this can help protect whatever you need to protect.
  3. A variation of the above, wallet keys can be used to ‘sign’ information (i.e., encrypt / decrypt). You could store the encrypted data in the blockchain and only allow the signer to access it, with their key (stored off the blockchain). See this code for more details
  4. One interesting approach is the commit reveal pattern. This pattern has limited application and is a bit complex but is a strategy that can be used hide information provided by a user until all users have submitted their input. (e.g., a blind auction)

Privacy of blockchain data is the bleeding-edge of an already bleeding-edge technology. Methods will continue to develop in the future to support the use-case of storing private data on the blockchain. However, until then…

Don’t keep secrets in storage

Join Coinmonks Telegram Channel and Youtube Channel get daily Crypto News

Also, Read

--

--