41. First depositor in a vault
In a new ERC-4626 vault, an attacker deposits 1 wei, then donates a large amount of assets directly to the vault before a victim deposits. What happens and how is it prevented?
- A.The victim's shares round down to zero or very few, and the attacker captures their deposit; prevent with virtual shares/decimals offset or a seed deposit
- B.Nothing happens, because donations are rejected by ERC-20 tokens
- C.The vault becomes paused automatically
- D.The attacker loses the donation and the victim gains it
Show answer
Answer: A. The victim's shares round down to zero or very few, and the attacker captures their deposit; prevent with virtual shares/decimals offset or a seed deposit
Shares are computed as assets * totalSupply / totalAssets. The donation inflates totalAssets so the victim's share count rounds down. OpenZeppelin's ERC4626 uses virtual shares and a decimals offset; protocols also seed the vault with an initial deposit.
42. Paying everyone in a loop
A contract pays rewards by looping over an array of all users and sending ETH to each in one transaction. As users grow, what breaks and what is the better design?
- A.Nothing breaks, because the EVM has no gas limit
- B.Only the first user is paid
- C.The loop eventually exceeds the block gas limit, and one reverting receiver can block everyone; use a pull pattern where each user claims their own reward
- D.Loops are not allowed in Solidity
Show answer
Answer: C. The loop eventually exceeds the block gas limit, and one reverting receiver can block everyone; use a pull pattern where each user claims their own reward
Unbounded loops grow gas use until the function cannot run, and a single contract that rejects ETH can make the whole payout revert. Pull-over-push lets users withdraw individually, and Merkle-based claims scale to millions of users.
43. Reentrancy through safeMint
A mint function limits each address to 1 NFT. An attacker's contract ends up with 20. How?
function mint() external {
require(minted[msg.sender] == 0, "already minted");
_safeMint(msg.sender, nextId++);
minted[msg.sender] = 1;
}- A.nextId++ overflows and resets the counter
- B._safeMint calls onERC721Received on the receiver before minted is set, so the attacker re-enters mint() from that callback
- C._safeMint is not allowed for contracts, so the check is skipped
- D.require does not work inside external functions
Show answer
Answer: B. _safeMint calls onERC721Received on the receiver before minted is set, so the attacker re-enters mint() from that callback
_safeMint (and safeTransferFrom) call onERC721Received on contract receivers, which is an external call to attacker code. Because minted is updated after that call, the attacker can call mint() again from the callback and pass the check each time. Update state before the mint (checks-effects-interactions) or add a reentrancy guard.
44. Using a signature as a nonce
A contract prevents replay by storing used[keccak256(signature)] = true. Why can an attacker still reuse a valid signature?
- A.keccak256 of a signature can collide easily
- B.Signatures expire after one block
- C.ECDSA signatures are malleable: (r, s) can be changed to (r, n - s) with a flipped v, which is a different byte string that recovers the same signer
- D.The contract cannot read the signature bytes
Show answer
Answer: C. ECDSA signatures are malleable: (r, s) can be changed to (r, n - s) with a flipped v, which is a different byte string that recovers the same signer
For secp256k1, both s and n - s give a valid signature for the same message and signer. So the attacker produces a second, different signature and its hash is not in the used set. Track replay with a nonce or the message hash instead, and use OpenZeppelin ECDSA, which rejects high-s values.
45. Hash collision with encodePacked
An allowlist checks keccak256(abi.encodePacked(name, role)) against signed hashes, where name and role are both strings. What can go wrong?
- A.Different pairs like ("ad", "min") and ("a", "dmin") produce the same packed bytes and the same hash, so one signature can authorize a different pair
- B.abi.encodePacked cannot take strings
- C.keccak256 returns a different hash each time for strings
- D.Packed encoding adds random padding, so hashes never match
Show answer
Answer: A. Different pairs like ("ad", "min") and ("a", "dmin") produce the same packed bytes and the same hash, so one signature can authorize a different pair
abi.encodePacked concatenates dynamic types with no length prefix, so the boundary between two strings is lost. Any split of the same characters hashes the same. Use abi.encode, which includes offsets and lengths, or structured EIP-712 hashing.
46. Constant product swap output
A constant product AMM (x * y = k) pool holds 100 ETH and 200,000 USDC. Ignoring fees, about how much USDC does a trader get for selling 10 ETH?
- A.About 18,182 USDC
- B.Exactly 20,000 USDC
- C.About 22,222 USDC
- D.About 19,900 USDC
Show answer
Answer: A. About 18,182 USDC
k = 100 * 200,000 = 20,000,000. After the trade the pool has 110 ETH, so it must keep 20,000,000 / 110 ≈ 181,818 USDC, and the trader receives about 200,000 - 181,818 = 18,182 USDC. The gap from the spot price of 20,000 is price impact, which grows as the trade gets larger relative to the pool.
47. Why transparent proxies split callers
Why does the transparent proxy pattern route admin calls to the proxy and all other calls to the implementation?
- A.To make admin calls cheaper
- B.To let the admin skip gas fees
- C.Because implementations cannot have external functions
- D.To avoid function selector clashes: if a proxy function and an implementation function share a 4-byte selector, users could accidentally trigger proxy admin logic
Show answer
Answer: D. To avoid function selector clashes: if a proxy function and an implementation function share a 4-byte selector, users could accidentally trigger proxy admin logic
Selectors are only 4 bytes, so two different functions can share one. The transparent pattern removes the ambiguity: the admin only reaches proxy functions, and everyone else is always forwarded. The cost is an extra admin check on every call, which is one reason UUPS became popular.
48. Expensive loop over storage
Which change saves the most gas in this function?
uint256[] public rewards;
uint256 public total;
function sum() external {
for (uint256 i = 0; i < rewards.length; i++) {
total += rewards[i];
}
}- A.Change i++ to ++i
- B.Load total into a local variable, read rewards.length once, add in memory, and write total to storage once at the end
- C.Make rewards private
- D.Use uint8 for i to save space
Show answer
Answer: B. Load total into a local variable, read rewards.length once, add in memory, and write total to storage once at the end
Each loop iteration currently reads rewards.length, reads total, and writes total, and storage access costs far more than stack or memory (cold SLOAD is 2,100 gas, and SSTORE is thousands). Caching values locally and doing one SSTORE at the end removes most of that cost. ++i saves only a few gas since 0.8.22, and a uint8 counter can even cost more.
49. Impermanent loss at a 4x price move
You deposit equal value of ETH and USDC into a 50/50 constant-product pool. ETH's price then rises 4x. Ignoring fees, how does your position compare with just holding?
- A.About 20% less than holding
- B.About 4% less than holding
- C.About 50% less than holding
- D.The same as holding
Show answer
Answer: A. About 20% less than holding
For a price ratio r, the loss versus holding is 2√r/(1+r) − 1. With r = 4 that is 2×2/5 − 1 = −0.2, so 20% less. The pool sells ETH as it rises, leaving you with less of the asset that went up. Fees may or may not make up for it.
50. Changing an allowance from 100 to 50
Alice approved Bob for 100 tokens. She now calls `approve(bob, 50)`. Bob watches the mempool. What is the worst case?
- A.Bob can spend at most 50 after the change
- B.Bob spends the 100 first, then the new 50, taking 150 in total
- C.The second approve reverts while the first is unused
- D.Bob's spend reverts because the allowances clash
Show answer
Answer: B. Bob spends the 100 first, then the new 50, taking 150 in total
approve overwrites the allowance. If Bob front-runs the change with transferFrom of 100, the new approve then sets a fresh 50 that he can also spend. Setting to zero first, using increase/decrease functions, or permit-style one-off approvals reduce this risk.
51. When collateral can be frozen
Your protocol holds users' USDC in one pool contract. Circle can freeze any address. What risk should your design consider?
- A.If the pool is frozen, every user's USDC is stuck at once
- B.Frozen USDC is automatically refunded to users
- C.USDC cannot be frozen once it is inside a contract
- D.Freezing only affects addresses outside the US
Show answer
Answer: A. If the pool is frozen, every user's USDC is stuck at once
USDC and USDT both have admin functions to blacklist addresses, including contracts. A pool holding everyone's funds could be frozen after a sanctions action or compromise. Teams weigh this when choosing collateral, splitting funds and planning governance responses.
52. A compromised npm package in a dapp
A popular wallet-connector npm package was briefly replaced with code that pops up a fake approval. What protects your dapp best against this kind of attack?
- A.Pinned versions, reviewed updates and a strict CSP
- B.Using only HTTPS for the whole website
- C.Asking users to check the URL bar every time
- D.Moving the dapp to a different hosting provider
Show answer
Answer: A. Pinned versions, reviewed updates and a strict CSP
Supply-chain attacks inject malicious code through dependencies, as with the Ledger Connect Kit incident in 2023. Pinned versions, lockfiles, delayed adoption of new releases, subresource integrity for CDN scripts and a content security policy limit what bad code can load or call. HTTPS does not help if the code itself is bad.
53. Modifier order and reentrancy
A function is declared as function claim() external updateReward(msg.sender) nonReentrant. updateReward calls an external rewards contract. Why is the order a problem?
- A.updateReward runs before the lock, so its call can re-enter
- B.Modifiers always run after the function body finishes
- C.nonReentrant only works when it is the last modifier
- D.Solidity runs modifiers in a random order each call
Show answer
Answer: A. updateReward runs before the lock, so its call can re-enter
Modifiers run left to right, wrapping the function. Here updateReward makes its external call before nonReentrant sets the lock, so a callback can re-enter claim or other guarded functions. Put nonReentrant first so the lock covers everything, and avoid external calls in modifiers.
54. Very large memory arrays
A function builds a huge in-memory array and gas use grows much faster than the array size. Why?
- A.Each memory word is written to storage too
- B.Memory reads become cold accesses after 1 KB
- C.The optimizer turns off for large arrays
- D.Memory expansion cost grows with the square of size
Show answer
Answer: D. Memory expansion cost grows with the square of size
Memory costs 3 gas per 32-byte word plus words^2 / 512, so the quadratic term dominates for large sizes. Gas therefore grows faster than linearly. Process data in chunks, avoid copying large arrays, or use calldata where possible.
55. Price that comes out as zero
This function returns 0 for amount = 5. Why, and what is the fix?
// price has 2 decimals: 1 unit = 100
function cost(uint256 amount) public pure returns (uint256) {
return amount / 10 * 100;
}- A.Solidity rounds 0.5 down only for pure functions
- B.The literal 100 must be written as 100e0
- C.5 / 10 truncates to 0 first; compute amount * 100 / 10
- D.The return type should be int256 for fractions
Show answer
Answer: C. 5 / 10 truncates to 0 first; compute amount * 100 / 10
Integer division truncates, so 5 / 10 is 0 and the multiplication cannot recover it. Multiplying first gives 500 / 10 = 50. For large values, multiplying first can overflow, so libraries offer mulDiv(a, b, c) that computes a x b / c with full 512-bit precision.
56. Indexed strings in events
An event declares string indexed name. An indexer reads the log and wants the actual name text. What does it find?
- A.The full string stored in the topic
- B.The first 32 bytes of the string
- C.Only keccak256 of the string in the topic
- D.The string length and the first character
Show answer
Answer: C. Only keccak256 of the string in the topic
Topics are 32 bytes, so indexed dynamic types like string, bytes and arrays are stored as their keccak256 hash. You can filter by an exact value, but you cannot read the original text from the topic. If you need both, emit the string twice: once indexed and once as a non-indexed field.
57. Storing claim flags cheaply
An airdrop must record whether each of 100,000 indexes has claimed. Which approach uses the least storage gas?
- A.A mapping(uint256 => uint256) bitmap: 256 flags per slot
- B.A mapping(uint256 => bool), one slot per index
- C.A bool[] array with one entry per index
- D.A string that lists all claimed indexes
Show answer
Answer: A. A mapping(uint256 => uint256) bitmap: 256 flags per slot
A bool in its own mapping entry uses a full 32-byte slot, so each first claim pays about 20,000 gas. A bitmap stores 256 flags in one word: word = index / 256, bit = index % 256, set with word |= 1 << bit. Only the first claim per word pays the zero-to-non-zero cost. OpenZeppelin's BitMaps does this.
58. A try/catch that still reverts
A contract wraps an external call in try/catch with a catch-all clause. The callee returns successfully but with malformed return data. What happens?
- A.The catch clause runs with the decode error
- B.Decoding fails in the caller and the whole call reverts
- C.The function continues with zero values
- D.The callee is retried once automatically
Show answer
Answer: B. Decoding fails in the caller and the whole call reverts
try/catch only catches reverts inside the called contract. Decoding the return data happens in the caller, and if it fails, that revert is not caught. Calling an address with no code can also slip past. Use a low-level call and decode carefully when the target is untrusted.
59. A catch clause that does not match
A try/catch has only catch Error(string memory reason). The callee reverts with a custom error InsufficientBalance(). What happens?
- A.Nothing matches, so the revert bubbles up
- B.The Error clause runs with an empty reason
- C.The custom error is turned into a string
- D.The transaction succeeds with no effect
Show answer
Answer: A. Nothing matches, so the revert bubbles up
catch Error(string) only matches revert("...") and require with a message. Panics need catch Panic(uint256), and custom errors need catch (bytes memory data) or a bare catch. With no matching clause, the error propagates and the caller reverts.
60. Secret user writing to an unclassified file
Under the Bell–LaPadula model, a user cleared for Secret tries to write into an Unclassified document. What does the model say?
- A.Allowed: higher clearance can always write anywhere
- B.Allowed: only reading down is restricted by the model
- C.Denied: no write down, to stop leaks to lower levels
- D.Denied: no read up, to protect higher levels
Show answer
Answer: C. Denied: no write down, to stop leaks to lower levels
Bell–LaPadula protects confidentiality with "no read up" (simple security property) and "no write down" (star property). Writing Secret knowledge into an Unclassified file could leak it, so it is denied. The Biba model does the reverse for integrity.