Chapter 14 Exercises: Advanced Solidity Patterns and Production Contracts
Exercise 14.1: Checks-Effects-Interactions Audit (Apply)
Review the following contract and identify all violations of the checks-effects-interactions pattern. For each violation, explain the attack vector it enables and rewrite the function correctly.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
contract VulnerableBank {
mapping(address => uint256) public balances;
uint256 public totalDeposited;
function deposit() external payable {
balances[msg.sender] += msg.value;
totalDeposited += msg.value;
}
function withdraw(uint256 amount) external {
require(balances[msg.sender] >= amount, "Insufficient balance");
(bool success, ) = msg.sender.call{value: amount}("");
require(success, "Transfer failed");
balances[msg.sender] -= amount;
totalDeposited -= amount;
}
function withdrawAll() external {
uint256 amount = balances[msg.sender];
require(amount > 0, "No balance");
(bool success, ) = msg.sender.call{value: amount}("");
require(success, "Transfer failed");
balances[msg.sender] = 0;
totalDeposited -= amount;
}
function emergencyTransfer(address payable to, uint256 amount) external {
require(balances[msg.sender] >= amount, "Insufficient");
to.call{value: amount}("");
balances[msg.sender] -= amount;
balances[to] += amount;
}
}
Tasks:
1. Identify all CEI violations (there are at least three).
2. Write an attacker contract that exploits the withdraw function via reentrancy.
3. Rewrite all four functions following the CEI pattern.
4. Add OpenZeppelin's ReentrancyGuard as an additional safety layer.
Expected deliverable: A corrected SecureBank.sol with all four functions rewritten and annotated with comments explaining the fix.
Exercise 14.2: Storage Packing Optimization (Analyze)
The following struct is used in a DeFi lending protocol. Each instance is stored per user. The protocol has 50,000 active users, so every wasted storage slot is multiplied 50,000 times.
struct LendingPosition {
uint256 collateralAmount; // Amount of collateral deposited
bool isActive; // Whether the position is open
address borrower; // Address of the borrower
uint256 borrowedAmount; // Amount borrowed
uint8 riskTier; // Risk classification (1-5)
uint256 lastUpdateTimestamp; // Last time the position was updated
bool isLiquidatable; // Whether the position can be liquidated
uint16 interestRateBps; // Interest rate in basis points (0-10000)
address collateralToken; // ERC-20 token used as collateral
uint256 accruedInterest; // Interest accrued since last update
}
Tasks: 1. Determine how many storage slots this struct currently occupies. Show your work by mapping each field to its slot number. 2. Reorder the fields to minimize the number of storage slots. Show the new slot assignments. 3. Calculate the gas savings per position for a function that reads all fields (assuming cold reads at 2,100 gas per slot). 4. Are there any fields where the type could be changed to enable further packing without losing meaningful precision? Justify your answer.
Exercise 14.3: Factory Pattern with Minimal Proxy (Create)
Build a CrowdfundFactory contract that uses the EIP-1167 minimal proxy (clone) pattern to deploy individual crowdfunding campaign contracts cheaply.
Requirements:
-
Create a
CrowdfundCampaigncontract with: - A beneficiary address (receives funds if goal is met) - A funding goal in wei - A deadline (block timestamp) - Acontribute()function that accepts Ether - AclaimFunds()function (only beneficiary, only after deadline, only if goal met) - Arefund()function (only contributors, only after deadline, only if goal NOT met) - Aninitializefunction (not a constructor — needed for the clone pattern) -
Create a
CrowdfundFactorycontract that: - Uses OpenZeppelin'sCloneslibrary to deploy minimal proxies - Maintains a registry of all deployed campaigns - Emits events for new campaign creation - Has agetCampaigns()view function -
Write a comparison showing the gas cost of deploying via
new CrowdfundCampaign(...)versusClones.clone(implementation).
Hint: Import @openzeppelin/contracts/proxy/Clones.sol and use Clones.clone(implementationAddress).
Exercise 14.4: UUPS Upgrade Simulation (Create)
Simulate a full upgrade lifecycle for a simple token contract.
Part A: Write TokenV1.sol:
- Inherits Initializable, UUPSUpgradeable, OwnableUpgradeable
- Has a name, symbol, and balances mapping
- Has an initialize function that sets name, symbol, and mints initial supply to the deployer
- Has transfer and balanceOf functions
Part B: Write TokenV2.sol:
- Preserves all V1 storage layout exactly
- Adds a paused state variable (appended after all V1 variables)
- Adds a pause() and unpause() function (only owner)
- Modifies transfer to check !paused
- Adds a burn(uint256 amount) function
Part C: Write a deployment and upgrade script (pseudocode or Hardhat script) that:
1. Deploys the V1 implementation
2. Deploys the ERC1967 proxy
3. Initializes through the proxy
4. Verifies V1 functionality
5. Deploys the V2 implementation
6. Calls upgradeToAndCall on the proxy to switch to V2
7. Verifies V2 functionality while confirming V1 state is preserved
Part D: Answer these questions:
1. What happens if you add the paused variable before balances in V2 instead of after? Be specific about which values would be corrupted.
2. What happens if you forget _disableInitializers() in the V2 constructor?
3. What happens if you remove _authorizeUpgrade from V2?
Exercise 14.5: Gas Optimization Challenge (Analyze/Create)
The following contract processes batch payments. It works correctly but is extremely gas-inefficient. Your task is to optimize it without changing its external behavior.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
contract IneffiecientPayments {
address public owner;
uint256 public totalPayments;
uint256 public paymentCount;
struct Payment {
address recipient;
uint256 amount;
bool processed;
string memo;
uint256 timestamp;
}
Payment[] public payments;
mapping(address => uint256[]) public recipientPayments;
constructor() {
owner = msg.sender;
}
modifier onlyOwner() {
require(msg.sender == owner, "Not owner");
_;
}
function batchPay(
address[] memory recipients,
uint256[] memory amounts,
string[] memory memos
) external payable onlyOwner {
require(recipients.length == amounts.length, "Length mismatch");
require(recipients.length == memos.length, "Length mismatch");
for (uint256 i = 0; i < recipients.length; i++) {
require(recipients[i] != address(0), "Zero address");
require(amounts[i] > 0, "Zero amount");
payments.push(Payment({
recipient: recipients[i],
amount: amounts[i],
processed: false,
memo: memos[i],
timestamp: block.timestamp
}));
recipientPayments[recipients[i]].push(payments.length - 1);
totalPayments += amounts[i];
paymentCount += 1;
(bool success, ) = recipients[i].call{value: amounts[i]}("");
require(success, "Payment failed");
payments[payments.length - 1].processed = true;
}
}
function getRecipientPayments(address recipient) external view returns (uint256[] memory) {
return recipientPayments[recipient];
}
}
Tasks: 1. List every gas inefficiency you can find (aim for at least 8). 2. Rewrite the contract with all optimizations applied. 3. For each optimization, estimate the approximate gas savings (order of magnitude is fine). 4. Identify any optimizations that trade off readability or functionality. For each, explain whether the trade-off is worthwhile.
Exercise 14.6: Oracle Integration — Multi-Feed Price Aggregator (Create)
Build a contract that reads from multiple Chainlink price feeds and implements safety checks.
Requirements:
- The contract reads ETH/USD and BTC/USD price feeds
- Implement a
getEthPrice()andgetBtcPrice()function with full validation: - Check that the round is complete (updatedAt > 0) - Check that the answer is not stale (configurable staleness threshold) - Check that the price is positive - CheckansweredInRound >= roundId - Implement a
getBtcEthPrice()function that derives the BTC/ETH price by dividing BTC/USD by ETH/USD, handling the decimal differences between feeds - Implement a
isPriceDeviated()function that compares the current price to the last stored price and returns true if the deviation exceeds a configurable threshold (useful for detecting oracle manipulation) - Include proper access control for setting the staleness threshold and deviation threshold
Bonus: Add a fallback mechanism — if the primary feed is stale, the contract reads from a backup feed address.
Exercise 14.7: Event-Driven Architecture (Apply)
Design the event schema for a decentralized marketplace contract. The marketplace supports listing items, making offers, accepting offers, and canceling listings.
Tasks:
- Define all events with appropriate parameters. For each event, justify which parameters are
indexedand which are not (remember the three-indexed-parameter limit). - Write the Solidity event declarations.
- Write a subgraph schema (GraphQL) that defines the entities needed to support these front-end queries: - All active listings for a specific seller - The price history of a specific item - The total volume traded by a specific buyer - The most recent 50 sales, sorted by price
- Write pseudo-code for the subgraph mapping functions that transform events into entity updates.
Exercise 14.8: Production Readiness Review (Evaluate)
You have been asked to review the following contract before mainnet deployment. Perform a production readiness assessment.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
contract SimpleStaking {
address public owner;
uint256 public rewardRate = 100; // rewards per second
mapping(address => uint256) public stakedBalance;
mapping(address => uint256) public lastStakeTime;
constructor() {
owner = msg.sender;
}
function stake() external payable {
if (stakedBalance[msg.sender] > 0) {
uint256 reward = (block.timestamp - lastStakeTime[msg.sender]) * rewardRate;
payable(msg.sender).transfer(reward);
}
stakedBalance[msg.sender] += msg.value;
lastStakeTime[msg.sender] = block.timestamp;
}
function unstake(uint256 amount) external {
require(stakedBalance[msg.sender] >= amount);
uint256 reward = (block.timestamp - lastStakeTime[msg.sender]) * rewardRate;
stakedBalance[msg.sender] -= amount;
payable(msg.sender).transfer(amount + reward);
}
function setRewardRate(uint256 newRate) external {
require(msg.sender == owner);
rewardRate = newRate;
}
function emergencyWithdraw() external {
require(msg.sender == owner);
payable(owner).transfer(address(this).balance);
}
}
Tasks: 1. Identify all security vulnerabilities (aim for at least 6). 2. Identify all missing production features (aim for at least 5). 3. Identify all gas optimization opportunities. 4. Rate the contract's production readiness on a scale of 1-10 and justify your rating. 5. Rewrite the contract to address all issues, using OpenZeppelin where appropriate.
Exercise 14.9: Comprehensive Design Pattern Application (Create)
Build a DecentralizedEscrow contract that demonstrates all five design patterns from Section 14.2.
Requirements:
- State machine: The escrow progresses through states: Created -> Funded -> Delivered -> Completed/Disputed/Refunded
- Checks-effects-interactions: All Ether transfers follow the CEI pattern
- Pull-over-push: Funds are not automatically sent; recipients withdraw them
- Access restriction: Buyer, seller, and arbiter have different permissions at different states
- Factory pattern: An
EscrowFactorycreates and tracks escrow instances
Functional requirements: - Buyer creates escrow, specifying seller, arbiter, and amount - Buyer funds the escrow - Seller marks as delivered - Buyer confirms delivery (releases funds to seller) OR disputes - If disputed, arbiter decides (release to seller or refund to buyer) - All transitions emit events - Include a timeout: if buyer does not confirm or dispute within 30 days of delivery, seller can claim funds
Deliverable: Two Solidity files (Escrow.sol and EscrowFactory.sol) with comprehensive NatDoc comments explaining how each pattern is applied.
Exercise 14.10: Research and Report — Upgrade Incidents (Evaluate)
Research one of the following real-world incidents involving proxy upgrades or governance:
- Option A: The Wormhole bridge hack ($320M, February 2022)
- Option B: The Compound governance proposal 62 ($80M, September 2021)
- Option C: The Ronin bridge hack ($625M, March 2022)
Write a 1,500-word incident report covering:
- What happened: Technical details of the exploit or error
- Root cause: Which specific pattern or practice was violated
- Impact: Financial and reputational consequences
- Response: How the team responded and what was recovered
- Lessons: What design patterns or practices from this chapter would have prevented or mitigated the incident
- Your analysis: Could a well-designed set of production safeguards (timelock, multi-sig, monitoring) have prevented this? Why or why not?
Sources: Use official post-mortems, security firm analyses (Rekt News, SlowMist, CertiK), and on-chain transaction analysis.