🌱 Beginner ⏱️ 25 min

Accounts, Wallets & Transactions

In Ethereum, there are two types of accounts — and understanding the difference is critical for security auditing. Every vulnerability related to authentication, access control, and authorization ultimately comes down to how accounts work and how transactions are processed.

EOA vs Contract Accounts — Full Comparison

FeatureExternally Owned Account (EOA)Contract Account
Controlled byA private key (a human or bot)Code (deployed bytecode)
Can initiate transactions?Yes — only EOAs can start txsNo — can only react to calls
Has code?NoYes (EVM bytecode)
Has storage?No (just balance)Yes (key-value store)
Can hold ETH?YesYes (if designed to)
Address derivationFrom public key (keccak256)From deployer address + nonce
ExampleMetaMask wallet, bot walletUniswap, USDC, Aave
Nonce meaningNumber of txs sentNumber of contracts created
📖 Every Blockchain Action Starts with an EOA

Smart contracts cannot spontaneously act. They can only execute when triggered by a transaction — and all transactions must be initiated by an EOA. This means that even complex DeFi protocols that call dozens of contracts in sequence are always started by a human (or a bot with a private key).

Public Key Cryptography — The Simple Explanation

Your Ethereum identity is based on asymmetric cryptography. You do not need to understand the math, but you need to understand the model:

🔑 The Key Pair Model
PRIVATE KEY (secret — never share this) - A random 256-bit number: 0x1a2b3c...f9e8 - This is your IDENTITY and your SIGNING AUTHORITY - Anyone with this can do anything your account can do - Lost private key = lost account forever PUBLIC KEY (safe to share — derived from private key) - Mathematically derived from the private key - Cannot be reversed: knowing the public key reveals NOTHING about the private key - Used by others to VERIFY your signatures SIGNATURE (proves ownership without revealing the key) - You: sign(message, privateKey) → signature - Anyone: verify(message, signature, publicKey) → true/false - The math guarantees: only the private key holder can produce a valid signature

Address Derivation — Step by Step

Your Ethereum address is derived deterministically from your private key. Here is the full process:

📍 From Private Key to Address
Step 1: Start with a private key (random 32 bytes) privateKey = 0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80 Step 2: Apply elliptic curve multiplication (secp256k1) publicKey = ECDSA_pubkey(privateKey) → This is a 64-byte (512-bit) uncompressed public key publicKey = 0x04 + x_coordinate(32 bytes) + y_coordinate(32 bytes) Step 3: Hash the public key with Keccak-256 hash = keccak256(publicKey) → 32-byte hash Step 4: Take the LAST 20 bytes of the hash address = "0x" + last_20_bytes(hash) → 0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266 Checksum encoding (EIP-55): Mixed-case version of the hex: 0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266 (Some letters capitalized based on hash — catches typos)
⚠️ Address Derivation Is One-Way

You CANNOT recover a private key from an address. If someone sends you their address, you know nothing about their private key. This is the mathematical guarantee that makes Ethereum secure. However, if you ever see a private key exposed on GitHub, in contract code, or in a transaction's input data — that account is compromised and any funds should be moved immediately.

What a Wallet Actually Is

One of the most common misconceptions in crypto: your coins are NOT stored in your wallet. The blockchain stores the balance. Your wallet stores and manages your private keys.

💰 What a Wallet Really Does
What MetaMask (or any wallet) does: 1. KEY MANAGEMENT: Stores your private key (encrypted with your password) 2. SIGNING: Signs transactions with your private key before broadcasting 3. BROADCASTING: Sends signed transactions to Ethereum nodes 4. BALANCE DISPLAY: Queries the blockchain to show your current balance 5. ADDRESS BOOK: Remembers addresses you've used What MetaMask does NOT do: ✗ Store your ETH (the blockchain does) ✗ Hold your tokens (contract state holds them) Analogy: A wallet is like a keychain for a safe-deposit box. The money is in the BANK (blockchain). The keychain (wallet) holds the KEY (private key). If you lose the keychain → you can still get in with the key itself If you lose the key (private key / seed phrase) → you're locked out forever

Transaction Anatomy — Every Field Explained

A signed Ethereum transaction contains these fields. Auditors must understand each one to trace exploits:

📜 Full Transaction Structure
// A complete Ethereum transaction (EIP-1559 format) { "from": "0xAlice...", // Recovered from signature (not in raw tx!) "to": "0xContractAddress", // Recipient — null for contract deployment "value": "1000000000000000000", // ETH in wei (1 ETH = 10^18 wei) "data": "0xa9059cbb...", // ABI-encoded function call (or bytecode) "nonce": 42, // Number of txs sent from this address "maxFeePerGas": "30000000000", // Max you'll pay per gas unit (in gwei) "maxPriorityFeePerGas": "2000000000", // Tip to validator "gasLimit": 100000, // Max gas units you'll consume "chainId": 1, // 1 = Mainnet, 11155111 = Sepolia "v": "0x1", // Signature component (recovery bit) "r": "0x3a7b...", // Signature component "s": "0xf29c..." // Signature component }

The tx.data Field — ABI Encoding

When you call a smart contract function, the function call is encoded into the data field of the transaction. Here is how it works:

🔎 ABI Encoding Breakdown
Calling: transfer(address recipient, uint256 amount) with: recipient = 0xBob..., amount = 1000 Step 1: Function selector (4 bytes) keccak256("transfer(address,uint256)") = 0xa9059cbb2ab09eb219583f4a59a5d0623ade346d962bcd4e46b11da047c9049b First 4 bytes: 0xa9059cbb Step 2: Encode arguments (32 bytes each, padded) address: 0x000000000000000000000000[Bob's address without 0x, 20 bytes] uint256: 0x00000000000000000000000000000000000000000000000000000000000003e8 Full tx.data: 0xa9059cbb 000000000000000000000000Bob_address_here... 00000000000000000000000000000000000000000000000000000000000003e8

Transaction Lifecycle — From Signature to Confirmation

🔄 Transaction Flow Diagram
1. CREATE: User constructs transaction object ↓ 2. SIGN: Wallet signs with private key → v, r, s values added ↓ 3. BROADCAST: Signed tx sent to an Ethereum node via JSON-RPC ↓ 4. MEMPOOL: Node validates tx (signature ok? nonce ok? balance ok?) → Valid tx added to the mempool (public waiting room) → MEV bots can see your tx here! (front-running risk) ↓ 5. INCLUDED: A validator selects your tx for the next block → EVM executes the transaction → State changes applied (balances updated, storage written) ↓ 6. CONFIRMED: Block propagated to all nodes, they accept it ↓ 7. FINALIZED: After ~13 minutes, block is economically finalized → Now practically irreversible

The Nonce — Replay Attack Prevention

Every transaction from an EOA has a nonce — a counter that increments with each transaction. This prevents replay attacks.

🛡️ Nonce Replay Protection
WITHOUT nonces (hypothetical attack): Alice sends 1 ETH to Bob → transaction signed Bob copies the signed transaction and resubmits it 10 times → Alice loses 10 ETH! WITH nonces: Alice's account nonce: 5 Alice sends 1 ETH to Bob → tx has nonce=5, gets included Alice's account nonce becomes: 6 Bob tries to replay the nonce=5 tx → REJECTED ("nonce too low") The network will only accept nonce=6 next from Alice Important: Nonces must be used in ORDER If Alice has nonce=5 and submits nonce=7 first, that tx sits in mempool until a nonce=6 tx is submitted → This can cause "stuck" transactions

msg.sender vs tx.origin — CRITICAL Security Difference

This is one of the most important distinctions in Solidity. msg.sender is the immediate caller of the function. tx.origin is the original EOA that started the entire transaction chain. They are the same for simple calls — but differ in contract-to-contract calls.

🗺️ msg.sender vs tx.origin Diagram
Simple call (EOA → Contract): Alice (EOA) → calls Victim.withdraw() Inside Victim: msg.sender = Alice, tx.origin = Alice Multi-hop call (EOA → Contract → Contract): Alice (EOA) → calls AttackContract.attack() → which calls Victim.withdraw() Inside Victim: msg.sender = AttackContract, tx.origin = Alice KEY INSIGHT: msg.sender tells you WHO called this function directly tx.origin tells you WHICH EOA started the entire transaction tx.origin is ALWAYS an EOA (contracts can't initiate transactions) msg.sender can be a CONTRACT

The tx.origin Authentication Bypass Exploit

🚨 Critical Vulnerability: tx.origin Authentication

Never use tx.origin for authentication. Here is a complete exploit:

💥 tx.origin Exploit — Full Example
// VULNERABLE CONTRACT contract VulnerableWallet { address public owner; constructor() { owner = msg.sender; } function withdraw(address payable dest) public { // BUG: uses tx.origin instead of msg.sender require(tx.origin == owner, "Not owner"); dest.transfer(address(this).balance); } } // ATTACKER CONTRACT contract Attacker { address victim; address payable attacker; constructor(address _victim) { victim = _victim; attacker = payable(msg.sender); } // Victim is tricked into calling this (e.g., via phishing) receive() external payable { // Victim (the wallet owner) calls this contract with ETH // tx.origin = wallet owner (they initiated the tx) // msg.sender inside VulnerableWallet = this contract (Attacker) // The require(tx.origin == owner) PASSES because tx.origin is the owner! VulnerableWallet(victim).withdraw(attacker); } } // ATTACK VECTOR: // 1. Attacker sends phishing link to wallet owner // 2. Owner clicks it, sends 0.001 ETH to Attacker contract // 3. Attacker.receive() is triggered — with owner as tx.origin // 4. Attacker drains the VulnerableWallet! // FIX: Replace tx.origin with msg.sender require(msg.sender == owner, "Not owner"); // Correct
✏️ Try It — Read a Real Transaction on Etherscan
1. Go to https://etherscan.io 2. Search for a known DeFi transaction hash, e.g.: 0x356cfd6f0bfe4d0e1de2de1df93c72e87f29b4e4f4afe66b1d8bfb33f3d1a2e 3. On the transaction page, find: - "From" (the EOA that sent the tx) - "To" (contract or EOA that received it) - "Value" (ETH sent) - "Input Data" → click "Decode Input Data" to see the function call - "Gas Used / Gas Limit" — efficiency ratio - "Nonce" — how many txs this address has sent Key exercise: Find the "Input Data" tab and decode it. You'll see the function name + arguments clearly.

Common Mistakes Section

⚠️ Mistake #1: Using tx.origin for Authentication

Always use msg.sender for auth checks, never tx.origin. The only legitimate use of tx.origin is to check "was this transaction started by an EOA (not a contract)?" — but even that pattern is problematic and should be avoided.

⚠️ Mistake #2: Assuming msg.sender is Always an EOA

In contract-to-contract calls, msg.sender is a contract address. If your function has logic like "only an EOA can call this", you cannot reliably enforce it on-chain. The common trick of checking msg.sender == tx.origin prevents contract callers, but note that some chains (with account abstraction) may change this assumption.

⚠️ Mistake #3: Thinking ETH is Stored in the Wallet App

ETH balances are stored in the blockchain's world state, indexed by address. Deleting MetaMask doesn't delete your ETH. Recovering a seed phrase on a new device gives you full access again. But lose the seed phrase → lose the ETH forever, no matter how much you paid for MetaMask Premium.

Summary / Key Takeaways

ConceptKey FactSecurity Relevance
EOAControlled by private key, initiates txsCompromised key = total loss
Contract AccountControlled by code, reactive onlyBugs in code = exploitable
Private Key256-bit random number, never shareLoss = permanent, no recovery
AddressLast 20 bytes of keccak256(pubkey)Pseudonymous, not anonymous
Transaction NonceIncrementing counter per senderPrevents replay attacks
msg.senderImmediate caller of the functionUse for auth, not tx.origin
tx.originEOA that initiated the entire chainNEVER use for authentication
tx.dataABI-encoded function callCan encode arbitrary calls