By Fatskills Exam Guides Team — the exam nerds behind 28,500+ quizzes and 2.1M practice questions across 500+ global exams.
Smart contract security is the discipline of designing, writing, and reviewing blockchain code so that the contract behaves exactly as the author intends even when hostile actors try to exploit it. Because contracts are immutable and control real value, a single bug can drain millions of dollars. A classic real‑world case is the DAO hack (2016), where an attacker repeatedly called a vulnerable splitDAO() function (a re‑entrancy flaw) and siphoned ~3.6 M ETH. Modern dApps—Uniswap swaps, NFT minting portals, DAO voting contracts—must embed proven defensive patterns from day one.
splitDAO()
Reentrancy Attack – A malicious contract invokes a vulnerable function again before the first call finishes, typically via call/fallback. solidity // vulnerable function withdraw(uint256 amount) external { require(balances[msg.sender] >= amount); (bool ok,) = msg.sender.call{value: amount}(""); require(ok); balances[msg.sender] -= amount; // <-- state change after external call }
call
fallback
solidity // vulnerable function withdraw(uint256 amount) external { require(balances[msg.sender] >= amount); (bool ok,) = msg.sender.call{value: amount}(""); require(ok); balances[msg.sender] -= amount; // <-- state change after external call }
Check‑Effects‑Interactions (CEI) – Defensive ordering: check conditions, effect state changes, then interact with external contracts. solidity function withdraw(uint256 amount) external { require(balances[msg.sender] >= amount, "Insufficient"); balances[msg.sender] -= amount; // effect (bool ok,) = msg.sender.call{value: amount}(""); require(ok, "Transfer failed"); // interaction }
solidity function withdraw(uint256 amount) external { require(balances[msg.sender] >= amount, "Insufficient"); balances[msg.sender] -= amount; // effect (bool ok,) = msg.sender.call{value: amount}(""); require(ok, "Transfer failed"); // interaction }
Overflow / Underflow – Arithmetic that wraps around the 256‑bit limit, turning uint256 max + 1 into 0. solidity // pre‑0.8.0 vulnerable uint256 total = a + b; // could overflow silently
uint256 max + 1
0
solidity // pre‑0.8.0 vulnerable uint256 total = a + b; // could overflow silently
SafeMath / Built‑in Checked Arithmetic – SafeMath library (pre‑0.8) or Solidity ≥0.8’s automatic revert on overflow. solidity using SafeMath for uint256; uint256 total = a.add(b); // reverts on overflow
SafeMath
solidity using SafeMath for uint256; uint256 total = a.add(b); // reverts on overflow
Front‑Running (MEV) – An attacker observes a pending transaction, then inserts their own transaction with a higher gas price to profit (e.g., sandwiching a Uniswap swap). js // simple JS snippet to detect a pending swap provider.on("pending", async (txHash) => { const tx = await provider.getTransaction(txHash); if (tx && tx.to === uniswapRouter && tx.data.includes("swapExactETHForTokens")) { // launch sandwich attack here } });
js // simple JS snippet to detect a pending swap provider.on("pending", async (txHash) => { const tx = await provider.getTransaction(txHash); if (tx && tx.to === uniswapRouter && tx.data.includes("swapExactETHForTokens")) { // launch sandwich attack here } });
Commit‑Reveal Scheme – A pattern that mitigates front‑running by separating commit (hash of secret) from reveal (the secret) across two blocks. solidity mapping(address => bytes32) public commitments; function commit(bytes32 hash) external { commitments[msg.sender] = hash; } function reveal(uint256 secret) external { require(commitments[msg.sender] == keccak256(abi.encodePacked(secret)), "Bad reveal"); // proceed with action }
solidity mapping(address => bytes32) public commitments; function commit(bytes32 hash) external { commitments[msg.sender] = hash; } function reveal(uint256 secret) external { require(commitments[msg.sender] == keccak256(abi.encodePacked(secret)), "Bad reveal"); // proceed with action }
Delegatecall Vulnerability – delegatecall runs code in the context of the calling contract, so a malicious library can overwrite storage slots. solidity // proxy pattern (bool success,) = implementation.delegatecall(msg.data);
delegatecall
solidity // proxy pattern (bool success,) = implementation.delegatecall(msg.data);
Access Control (Ownable / Roles) – Restricts privileged functions to specific addresses; misuse can open backdoors. solidity contract MyToken is ERC20, Ownable { function mint(address to, uint256 amount) external onlyOwner { _mint(to, amount); } }
solidity contract MyToken is ERC20, Ownable { function mint(address to, uint256 amount) external onlyOwner { _mint(to, amount); } }
Static Analyzer (Slither, MythX, Oyente) – Automated tools that scan Solidity for known anti‑patterns (reentrancy, uninitialized storage, etc.).
Formal Verification (Certora, VeriSol) – Proving mathematically that a contract satisfies invariants; used for high‑value protocols (e.g., Curve, Aave).
EIP‑2929 (Gas Cost Changes) – New gas cost rules for SLOAD/SSTORE that affect how you write cheap “checks” and can unintentionally expose re‑entrancy windows.
SLOAD
SSTORE
Ownable
ReentrancyGuard
solhint
npx hardhat compile
require
slither .
mythx analyze ./contracts
npx hardhat run scripts/deploy.js --network goerli
node
hardhat flatten
Mistake: Using tx.origin for authentication (e.g., require(tx.origin == owner)). Correction: Use msg.sender and proper role‑based access (onlyOwner). tx.origin can be spoofed through a phishing contract, letting an attacker bypass checks.
tx.origin
require(tx.origin == owner)
msg.sender
onlyOwner
Mistake: Placing state updates after an external call (the classic re‑entrancy pattern). Correction: Follow the CEI pattern; move all balance changes before any call, transfer, or send.
transfer
send
Mistake: Relying on assert for input validation. Correction: assert is meant for internal invariants and consumes all remaining gas on failure. Use require for user‑controlled checks to get a clean revert and a meaningful error string.
assert
Mistake: Forgetting to set the Solidity compiler version (pragma solidity ^0.8.0;) and thus compiling with an older version that lacks built‑in overflow checks. Correction: Pin to a recent stable version (e.g., pragma solidity 0.8.20;) and enable optimizer only after testing.
pragma solidity ^0.8.0;
pragma solidity 0.8.20;
optimizer
Mistake: Hard‑coding gas limits for external calls (call{gas: 5000}) without understanding EIP‑2929. Correction: Let the EVM manage gas (call{value: amount}) or explicitly forward gasleft(); otherwise you may open a re‑entrancy window or cause out‑of‑gas failures.
call{gas: 5000}
call{value: amount}
gasleft()
call executes code in the callee’s context (its storage, address). delegatecall runs the callee’s code as if it were part of the caller, so storage layout must match; safe only when the library is trusted and immutable.
“How would you mitigate front‑running on a token swap?”
Use a commit‑reveal scheme, add a slippage buffer, or route the swap through a private transaction pool (e.g., Flashbots). Demonstrate awareness of MEV and gas‑price bidding.
“What does the ReentrancyGuard modifier do under the hood?”
It sets a storage flag (_status) to ENTERED before the function body runs and reverts if the flag is already set, preventing nested entry.
_status
ENTERED
“Why is ERC‑777 considered safer than ERC‑20 for token transfers?”
hooks
tokensReceived
Scenario: A contract uses require(tx.origin == admin) to restrict a withdraw() function. Answer: Dangerous – an attacker can trick a user (the admin) into calling a malicious contract that then calls withdraw(), because tx.origin stays the original EOA.
require(tx.origin == admin)
withdraw()
Scenario: You see uint256 total = a + b; in a Solidity 0.7 contract with no SafeMath. Answer: Vulnerable to overflow – the addition could wrap to zero, letting an attacker inflate balances; upgrade to Solidity ≥0.8 or wrap with SafeMath.
uint256 total = a + b;
Scenario: A DeFi pool’s addLiquidity() updates the pool’s token balance after calling token.transferFrom(). Answer: Re‑entrancy risk – a malicious ERC‑20 token could re‑enter addLiquidity() during transferFrom, causing double‑counting. Move the state update before the external call.
addLiquidity()
token.transferFrom()
transferFrom
_NOT_ENTERED
_ENTERED
mapping
slither
mythx
Join 4M+ learners. Unlock unlimited quizzes, wrong-answer tracking, flashcards + reminders, study guides, and 1-on-1 challenges.