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
| Feature | Externally Owned Account (EOA) | Contract Account |
|---|---|---|
| Controlled by | A private key (a human or bot) | Code (deployed bytecode) |
| Can initiate transactions? | Yes — only EOAs can start txs | No — can only react to calls |
| Has code? | No | Yes (EVM bytecode) |
| Has storage? | No (just balance) | Yes (key-value store) |
| Can hold ETH? | Yes | Yes (if designed to) |
| Address derivation | From public key (keccak256) | From deployer address + nonce |
| Example | MetaMask wallet, bot wallet | Uniswap, USDC, Aave |
| Nonce meaning | Number of txs sent | Number of contracts created |
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:
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 signatureAddress Derivation — Step by Step
Your Ethereum address is derived deterministically from your private key. Here is the full process:
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)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 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 foreverTransaction Anatomy — Every Field Explained
A signed Ethereum transaction contains these fields. Auditors must understand each one to trace exploits:
// 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:
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...
00000000000000000000000000000000000000000000000000000000000003e8Transaction Lifecycle — From Signature to Confirmation
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 irreversibleThe Nonce — Replay Attack Prevention
Every transaction from an EOA has a nonce — a counter that increments with each transaction. This prevents replay attacks.
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" transactionsmsg.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.
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 CONTRACTThe tx.origin Authentication Bypass Exploit
Never use tx.origin for authentication. Here is a complete exploit:
// 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"); // Correct1. 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
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.
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.
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
| Concept | Key Fact | Security Relevance |
|---|---|---|
| EOA | Controlled by private key, initiates txs | Compromised key = total loss |
| Contract Account | Controlled by code, reactive only | Bugs in code = exploitable |
| Private Key | 256-bit random number, never share | Loss = permanent, no recovery |
| Address | Last 20 bytes of keccak256(pubkey) | Pseudonymous, not anonymous |
| Transaction Nonce | Incrementing counter per sender | Prevents replay attacks |
| msg.sender | Immediate caller of the function | Use for auth, not tx.origin |
| tx.origin | EOA that initiated the entire chain | NEVER use for authentication |
| tx.data | ABI-encoded function call | Can encode arbitrary calls |