Ethernaut(Lvl 8): How Private is "private" variables?
Understanding Ethereum storage
To understand ethereum storage requires delving into two distinct aspects:
Firstly, it involves comprehending how Ethereum stores contract data on the blockchain. Secondly, it encompasses understanding how Solidity handles global and local variables. In this discussion, we'll focus on exploring how Ethereum stores data on the blockchain.
Storage on the Ethereum blockchain consists of 2^256 slots, each spanning 32 bytes. Each smart contract possesses its own storage, which serves as a reflection of the contract's state. Notably, the values stored in this storage persist across various function calls and are intrinsically linked to the smart contract's address.
Physically, data is stored sequentially within these slots, following the order of declaration. Storage optimization techniques are employed to conserve byte space. Consequently, if sequential variables can fit within a single 32-byte slot, they will share the same slot, with indexing occurring from the least significant bits (from the right). This storage and space optimization strategy is visually represented, wherein boolean and uint16 variables share slot 0, resulting in reduced gas costs for contract instantiation.
To access storage conveniently, Web3 provides a mechanism to retrieve data from contract storage using the web3.eth.getStorageAt(contractAddress, slotNumber)
function.
The Challenge
The challenge involves unlocking a vault smart contract by providing the password which is stored as a private variable in the same smart contract
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract Vault {
bool public locked;
bytes32 private password;
constructor(bytes32 _password) {
locked = true;
password = _password;
}
function unlock(bytes32 _password) public {
if (password == _password) {
locked = false;
}
}
}
The password resides in storage, and it's important to note that everything stored on the blockchain is publicly accessible. While the private visibility modifier prevents other contracts from accessing it, using web3.js(through the console) still enables us to read the storage variable. Notably, the password is stored at storage slot 1.
// The password is stored at storage slot 1
const password = await web3.eth.getStorageAt(challenge.address, 1);
console.log(`The password is: ${password} "${Buffer.from(password.slice(2), `hex`)}"`);
//The password can then be passed into the unlock function
Developer TidBits
On the blockchain, all storage is publicly visible, including your private variables. It's crucial to never store passwords and private keys without hashing them first for added security. Additionally, exercise caution when employing delegatecall
with contracts that contain storage variables to avoid potential data corruption issues.