Chapter 14 Quiz: Advanced Solidity Patterns and Production Contracts
Question 1
What is the correct ordering in the checks-effects-interactions (CEI) pattern?
A) Validate inputs, make external calls, update state variables B) Update state variables, validate inputs, make external calls C) Validate inputs, update state variables, make external calls D) Make external calls, validate inputs, update state variables
Answer: C
Explanation: The CEI pattern mandates: (1) Checks — validate all conditions using require statements and input validation; (2) Effects — update all state variables; (3) Interactions — make external calls such as sending Ether or calling other contracts. This ordering ensures that state is consistent before any external call transfers execution control to potentially malicious code. Placing the external call last means that even if the recipient calls back into the contract (reentrancy), the state has already been updated, preventing the reentrant call from exploiting stale state.
Question 2
In the pull-over-push pattern, why is the "push" approach dangerous for refunding multiple addresses?
A) It costs more gas than the pull approach B) If any single recipient rejects Ether, the entire batch transaction reverts, potentially freezing the contract C) It requires the contract to store more data D) It violates the ERC-20 standard
Answer: B
Explanation: When a contract pushes Ether to multiple recipients in a single transaction (e.g., refunding auction bidders), if any one recipient is a contract that reverts on receiving Ether (either intentionally as an attack or due to lacking a receive/fallback function), the entire transaction reverts. This means a single malicious or malfunctioning recipient can permanently freeze the contract by making it impossible to process any refunds. The pull pattern avoids this by having each recipient initiate their own withdrawal — if one recipient's withdrawal fails, it only affects that recipient.
Question 3
What does delegatecall do differently from a regular call?
A) It sends more gas to the target contract B) It executes the target contract's code using the calling contract's storage, msg.sender, and msg.value C) It creates a new contract at a new address D) It executes the calling contract's code using the target contract's storage
Answer: B
Explanation: delegatecall executes the target contract's bytecode but in the context of the calling contract. This means the target's code reads from and writes to the calling contract's storage, and msg.sender and msg.value remain those of the original caller. This is the mechanism that makes proxy patterns work: the proxy (calling contract) holds all the state, and the implementation (target contract) provides the logic that operates on that state. When upgrading, you change which implementation the proxy delegates to, and the new logic operates on the same stored state.
Question 4
Why do upgradeable contracts use initialize functions instead of constructors?
A) Constructors cost more gas B) Constructors cannot accept parameters C) Constructor code executes during deployment and writes to the implementation's storage, not the proxy's storage D) The Solidity compiler does not support constructors in upgradeable contracts
Answer: C
Explanation: When a contract with a constructor is deployed, the constructor executes once and its effects are stored in that contract's storage. For an implementation contract behind a proxy, this means the constructor's effects are stored in the implementation's storage, which is never used — the proxy's storage is what matters. Initializer functions are regular functions that, when called through the proxy via delegatecall, write to the proxy's storage. OpenZeppelin's Initializable base contract provides the initializer modifier to ensure the function can only be called once, mimicking the one-time-execution guarantee of constructors.
Question 5
What is the primary advantage of UUPS over transparent proxy?
A) UUPS supports more programming languages B) UUPS has lower gas cost per user call because the proxy contains no admin-check logic C) UUPS contracts cannot be upgraded, making them more secure D) UUPS proxies store data more efficiently
Answer: B
Explanation: In the transparent proxy pattern, every call to the proxy checks whether the caller is the admin to decide whether to execute proxy management functions or delegate to the implementation. This admin check adds approximately 2,600 gas to every user call. In UUPS, the proxy is minimal (only delegatecall forwarding), and the upgrade logic lives in the implementation contract. User calls go directly to delegatecall without any admin check, resulting in lower gas per call. Additionally, UUPS proxies have smaller bytecode (lower deployment cost) and can be made permanently non-upgradeable by deploying an implementation without upgrade logic.
Question 6
Which of the following is the MOST expensive operation in terms of gas on the EVM?
A) Reading a local memory variable B) Performing an addition on two uint256 values C) Writing a non-zero value to a previously-zero storage slot (SSTORE) D) Reading a constant variable
Answer: C
Explanation: An SSTORE operation that writes a non-zero value to a previously-zero storage slot costs 20,000 gas, making it one of the most expensive common operations in the EVM. By comparison, reading from memory costs approximately 3 gas, arithmetic operations cost 3-5 gas, and reading a constant variable costs 3 gas (since constants are embedded in bytecode as PUSH operations). This is why minimizing storage writes is the single most impactful gas optimization strategy.
Question 7
What is storage packing in Solidity?
A) Compressing data before storing it on-chain B) Arranging state variables so that multiple small variables share a single 256-bit storage slot C) Using arrays instead of mappings to store data D) Storing data in events instead of storage
Answer: B
Explanation: The EVM operates on 256-bit (32-byte) storage slots. Each slot read or write is a discrete gas-costing operation. Storage packing arranges state variable declarations so that variables whose combined size is 32 bytes or less are placed adjacent to each other, allowing the Solidity compiler to store them in a single slot. For example, a uint128 (16 bytes) and another uint128 (16 bytes) declared adjacently will share one slot. Without packing, each variable might occupy its own slot, doubling the gas cost of reads and writes.
Question 8
Why is call{value: amount}("") recommended over transfer(amount) for sending Ether?
A) call is cheaper in gas
B) call prevents reentrancy attacks automatically
C) transfer forwards only 2,300 gas, which may be insufficient for recipient contracts after gas cost changes in EIP-1884
D) call does not require the recipient to have a receive function
Answer: C
Explanation: transfer() forwards exactly 2,300 gas to the recipient, which was originally enough for a simple receive function to emit an event. However, EIP-1884 (Istanbul hard fork, 2019) increased the gas cost of SLOAD from 200 to 800, meaning recipient contracts that read storage in their receive function now cost more than 2,300 gas. This makes transfer() prone to breaking on future gas cost changes. call{value: amount}("") forwards all available gas (or a configurable amount) and returns a boolean success indicator. However, because it forwards more gas, it requires reentrancy protection (CEI pattern or ReentrancyGuard) to be safe.
Question 9
What is the "oracle problem" in blockchain?
A) Oracle database software is incompatible with Ethereum B) Smart contracts cannot natively access data from outside the blockchain, and external data feeds introduce trust assumptions C) Oracle nodes are too expensive to operate D) Oracles slow down block production
Answer: B
Explanation: The oracle problem is a fundamental challenge in blockchain design. Smart contracts execute deterministically — every node must arrive at the same result when processing a transaction. External API calls would return different results at different times on different nodes, breaking consensus. Oracles solve this by having trusted (or trust-minimized) operators write external data (prices, weather, sports scores) to on-chain contracts that other contracts can read. However, this introduces trust assumptions: the contract must trust that the oracle operators are honest and that the data sources are accurate. The entire security of DeFi protocols that depend on price feeds rests on these trust assumptions.
Question 10
When using Chainlink's latestRoundData(), which of the following checks is MOST critical for production code?
A) Checking that the returned roundId is an even number
B) Checking that updatedAt > 0 and that the data is not stale (e.g., block.timestamp - updatedAt < threshold)
C) Checking that the gas cost of the call is below 100,000
D) Checking that the feed address matches the Chainlink website
Answer: B
Explanation: latestRoundData() can return stale data (from a previous round that has not been updated recently), incomplete round data (where updatedAt is zero), or even negative/zero prices in edge cases. Production code must verify: (1) the round is complete (updatedAt > 0), (2) the data is fresh (block.timestamp - updatedAt < stalenessThreshold), (3) the price is positive (price > 0), and (4) the answer was provided in the current or later round (answeredInRound >= roundId). Protocols that skip these checks have been exploited during oracle outages when stale prices diverged from actual market prices.
Question 11
In an upgradeable contract, what happens if you insert a new state variable BEFORE existing variables in V2?
A) Nothing — the compiler handles it automatically B) The new variable overlaps with the first V1 variable's storage slot, corrupting both values C) The contract refuses to compile D) All existing data is automatically migrated to new positions
Answer: B
Explanation: The EVM stores state variables in numbered slots starting from slot 0. When a proxy delegates to a new implementation, the implementation's code accesses the proxy's storage by slot number, not by variable name. If V1 has uint256 value in slot 0 and V2 inserts a new uint256 newVar before it, then newVar occupies slot 0 and value moves to slot 1. But the proxy's slot 0 still contains the old value data. Now V2's newVar reads V1's value data, and V2's value reads whatever was in slot 1 (possibly the old owner address). This storage collision corrupts data silently — there is no error or revert.
Question 12
What is the purpose of _disableInitializers() in an upgradeable contract's constructor?
A) It prevents the contract from being deployed B) It prevents anyone from calling the initialize function directly on the implementation contract, closing a critical security hole C) It makes the contract cheaper to deploy D) It automatically calls the initialize function
Answer: B
Explanation: In a UUPS proxy setup, the implementation contract is deployed separately from the proxy. If the implementation's initialize function is left callable, an attacker can call it directly on the implementation contract (not through the proxy) and set themselves as the owner of the implementation. Depending on the implementation's code, this can enable attacks on the proxy. This is exactly what happened in the Wormhole hack ($320M). Calling _disableInitializers() in the constructor marks the implementation contract as already initialized, preventing any direct calls to initialize. Users interact through the proxy, which has its own initialization state.
Question 13
What is the maximum number of indexed parameters allowed per event in Solidity?
A) 1 B) 2 C) 3 D) Unlimited
Answer: C
Explanation: Solidity events can have up to three indexed parameters (plus the event signature itself, which is automatically topic[0]). Indexed parameters are stored as topics in the transaction log's Bloom filter, enabling efficient searching and filtering. Non-indexed parameters are stored in the event's data field and cannot be filtered directly — they must be retrieved and filtered client-side. Choosing which parameters to index requires balancing query performance (indexed enables filtering) against gas cost (indexed parameters cost slightly more gas) and the three-parameter limit.
Question 14
Which of the following is NOT a benefit of using the OpenZeppelin library?
A) Contracts are professionally audited and battle-tested B) Implementations follow ERC standards for interoperability C) OpenZeppelin contracts are guaranteed to be gas-optimal for every use case D) The community of thousands of developers provides continuous review
Answer: C
Explanation: While OpenZeppelin contracts are well-optimized, they are designed for generality and safety, not for maximum gas efficiency in every specific use case. Advanced protocols sometimes fork OpenZeppelin and optimize critical paths for their specific needs. For example, Uniswap V4 uses custom token transfer logic that is more gas-efficient than the standard ERC-20 implementation for their specific use case. The real benefits of OpenZeppelin are battle-testing (deployed on mainnet securing billions), professional audits, standard compliance, and community review — not a guarantee of optimal gas for every scenario.
Question 15
A DeFi protocol uses a transparent proxy with a 48-hour timelock on upgrades and a 3-of-5 multi-sig as the admin. An attacker compromises two of the five multi-sig keys. What can the attacker do?
A) Immediately upgrade the contract and drain user funds B) Nothing — they need three keys, and even then the timelock gives users 48 hours to withdraw C) Bypass the timelock using the two compromised keys D) Directly modify the proxy's storage
Answer: B
Explanation: This question illustrates the defense-in-depth approach discussed in the production deployment checklist. With a 3-of-5 multi-sig, the attacker needs three keys to propose an upgrade — two are insufficient. Even if three keys were compromised, the 48-hour timelock means the upgrade proposal is publicly visible for 48 hours before it can be executed. During this window, monitoring systems can detect the malicious proposal, the community can be alerted, and users can withdraw their funds. This is why production protocols layer multiple safeguards: multi-sig prevents single-key compromise, and timelocks provide a response window even if the multi-sig is compromised.
Question 16
Which gas optimization technique replaces storage reads (2,100 gas each) with bytecode reads (3 gas each)?
A) Storage packing
B) Using calldata instead of memory
C) Declaring variables as constant or immutable
D) Using unchecked arithmetic
Answer: C
Explanation: Variables declared as constant (value known at compile time) or immutable (value set in constructor) are embedded directly in the contract's bytecode rather than stored in contract storage. Reading them requires only a PUSH opcode (3 gas) instead of an SLOAD opcode (2,100 gas for a cold read). This is a 700x cost reduction per read. Any value that never changes after deployment should be declared constant or immutable — there is no reason to pay storage read costs for values that cannot change.
Question 17
In the factory pattern, what is the primary advantage of using EIP-1167 minimal proxy (clone) deployments over new Contract(...) deployments?
A) Clone contracts are more secure B) Clone deployment costs roughly 10x less gas because only a tiny proxy is deployed, not the full contract bytecode C) Clone contracts have more features D) Clone contracts can be upgraded
Answer: B
Explanation: The EIP-1167 minimal proxy pattern deploys a tiny contract (roughly 45 bytes of bytecode) that delegates all calls to a single implementation contract, instead of deploying the full bytecode of the child contract (which could be thousands of bytes). Since deployment gas cost is proportional to bytecode size, clone deployment is dramatically cheaper — roughly 10x less gas. All clones share the same implementation logic but maintain their own storage. This pattern is ideal for factory contracts that deploy many instances of the same contract (e.g., Uniswap pair factories, DAO frameworks, crowdfunding platforms).
Question 18
What is the role of Chainlink Automation (formerly Keepers) in smart contract development?
A) It provides random number generation B) It monitors on-chain conditions and executes contract functions when those conditions are met, since contracts cannot trigger their own execution C) It provides price data from external exchanges D) It manages multi-sig wallets
Answer: B
Explanation: Smart contracts are reactive — they execute in response to transactions, but they cannot initiate transactions themselves. This means time-based actions (closing a voting period, triggering a liquidation, executing a scheduled payment) require an external actor to submit the triggering transaction. Chainlink Automation provides this service through a decentralized network of nodes that monitor on-chain conditions specified by the contract (via checkUpkeep) and execute the contract's action function (performUpkeep) when conditions are met. This removes the need for centralized cron jobs or bots, which are single points of failure.
Question 19
A contract inherits from both ReentrancyGuard and Pausable. A function is marked with both nonReentrant and whenNotPaused. In what order are the modifiers executed?
A) Alphabetical order B) The order they appear in the function declaration, left to right C) The order they appear in the inheritance list D) Random order determined at compile time
Answer: B
Explanation: In Solidity, when multiple modifiers are applied to a function, they execute in the order they are listed in the function declaration, from left to right. For function foo() external nonReentrant whenNotPaused { ... }, nonReentrant executes first (setting the reentrancy lock), then whenNotPaused (checking the pause state), then the function body. The _ placeholder in each modifier represents "continue to the next modifier or the function body." This ordering can matter — if whenNotPaused were first and nonReentrant second, a paused contract would revert at the pause check without ever engaging the reentrancy lock, which is slightly more gas-efficient for reverted calls.
Question 20
You are designing a governance contract for a DeFi protocol with $500M in TVL. Which combination of production safeguards provides the strongest defense-in-depth?
A) Single owner key with a strong password B) 3-of-5 multi-sig with a 48-hour timelock, Pausable emergency stop, Forta monitoring, and an Immunefi bug bounty C) Access restricted to the deployer address with no timelock for fast response D) A fully decentralized DAO with no admin keys and no upgrade capability
Answer: B
Explanation: Option B provides layered defenses: the multi-sig prevents single-key compromise; the timelock provides a response window for users and monitors; the Pausable emergency stop enables rapid response to active exploits; Forta monitoring provides real-time alerting for suspicious transactions; and the Immunefi bug bounty incentivizes white-hat hackers to report vulnerabilities before they are exploited. Option A is dangerously centralized. Option C sacrifices all protection for speed. Option D sounds ideal in theory but is impractical for a $500M protocol that may need emergency responses to vulnerabilities — fully immutable, adminless contracts are appropriate for simple, well-audited systems but not for complex protocols that may need bug fixes.