Lecture 5: Solidity II: Contracts in Practice
Instructor: Yu Feng, UCSB
CS190: Blockchain Programming and Applications
Welcome to Lecture 5, where we'll explore advanced programming patterns in Solidity—Inheritance, Events, Libraries, and Security Patterns.
Motivation
Move beyond single-contract logic to build modular DeFi-scale systems using professional architectural patterns.
Inheritance
Reuse code and create policy layers
Events
Connect on-chain and off-chain systems
Libraries + Guards
Enable safe code reuse patterns
Interfaces
Build protocol composability
Goal: design secure, maintainable architectures for production DeFi applications.
Why Inheritance?
Encapsulate Reusable Behavior
Common patterns like Ownable and ERC20 become building blocks you can extend across multiple contracts.
Define Clear Hierarchies
Create "is-a" relationships for ownership, pausability, and metadata that make contract roles explicit.
Reduce Duplication
Centralize policy logic in base contracts, eliminating redundant code and reducing maintenance burden.
Example: Ownable Pattern
Base Contract
contract Ownable { address public owner; modifier onlyOwner() { require(msg.sender == owner, "Not owner"); _; } constructor() { owner = msg.sender; } }
Child Contract
contract MyVault is Ownable { function emergencyWithdraw() external onlyOwner { // Admin-only logic } }
Base = policy, child = logic
Common building block for admin control across DeFi protocols.
Overriding and super
contract Token { function transfer(address to, uint a) public virtual { ... } } contract AuditedToken is Token { event TransferAudited(address f, address t, uint a); function transfer(address to, uint a) public override { emit TransferAudited(msg.sender, to, a); super.transfer(to, a); } }
1
virtual
Allow override
2
override
Replace parent implementation
3
super
Extend, not rewrite
C3 Linearization Example
contract Base { function foo() public virtual {} } contract A is Base { function foo() public virtual override {} } contract B is Base { function foo() public virtual override {} } contract Final is A, B { function foo() public override(A,B) { super.foo(); } }
Deterministic Order
Solidity's C3 linearization provides predictable method resolution.
super.foo() → next in chain = B.foo()
Why Events?
01
Contracts Can't Push Data
Smart contracts have no way to proactively notify external systems of state changes.
02
Events = Cheap Structured Logs
Emit events to create on-chain logs that are inexpensive and queryable.
03
Power UI Updates & Analytics
Use for frontend updates, indexers, and analytics dashboards.
04
Always Emit After State Change
Ensure events reflect the actual final state to maintain data integrity.
Declaring & Subscribing to Events
Solidity Contract
event Deposit(address from, uint amount); function deposit() external payable { emit Deposit(msg.sender, msg.value); }

Client Example (Ethers.js)
vault.on("Deposit", (from, amount) => { console.log(`${from} deposited ${ethers.formatEther(amount)} ETH`); });
Why Libraries?
Reuse Common Logic
Libraries provide stateless helper functions that can be used across multiple contracts without inheritance overhead.
Prefer Over Inheritance
For utility functions, libraries are cleaner than inheritance chains and don't add to contract size.
Battle-Tested Examples
SafeMath, Strings, Address, and ReentrancyGuard are industry-standard library patterns.
Library + Reentrancy Guard Implementations
SafeMath Library
library SafeMath { function add(uint a, uint b) internal pure returns(uint) { return a + b; } }
Safe arithmetic operations prevent overflow vulnerabilities.
ReentrancyGuard
abstract contract ReentrancyGuard { uint private locked; modifier nonReentrant() { require(locked == 0, "reentrancy"); locked = 1; _; locked = 0; } }
locked acts as mutex for critical sections.
Using Libraries and Guards
using SafeMath for uint256; contract Vault is ReentrancyGuard { mapping(address => uint) balances; function deposit() external payable { balances[msg.sender] = balances[msg.sender].add(msg.value); } function withdraw(uint a) external nonReentrant { require(balances[msg.sender] >= a, "Insufficient"); balances[msg.sender] -= a; (bool ok,) = msg.sender.call{value: a}(""); require(ok, "Transfer failed"); } }
Combine utility + guard for secure value flow – protecting against both arithmetic errors and reentrancy attacks.
Why Composability Matters
DeFi thrives on protocols calling protocols – Aave, Uniswap, and Yearn integrate seamlessly through standardized interfaces.
DEX
Decentralized exchanges for token swaps
Lending
Borrow and lend protocols
Oracle
Price feed data providers
Stablecoin
Pegged currency protocols
Cross-protocol interactions use interfaces, not inheritance, enabling permissionless integration.
Using Interfaces
interface IERC20 { function transfer(address to, uint a) external returns(bool); function approve(address s, uint a) external returns(bool); } contract Treasury { IERC20 public token; constructor(address _t) { token = IERC20(_t); } function deposit(uint a) external { token.transferFrom(msg.sender, address(this), a); } }
Define expected ABI → plug-and-play modules
Foundation for ERC-standard interoperability across the entire DeFi ecosystem.
Best Practices for DeFi Developers
Use interfaces for cross-protocol calls
Enable composability and reduce coupling between contracts.
Emit events for transparency
Create audit trails and enable off-chain monitoring.
Guard withdraws with nonReentrant
Protect against reentrancy attacks on value transfers.
Avoid loops over user lists
Unbounded loops can exceed gas limits and brick contracts.
Fork-test mainnet integrations
Test against real protocol state before deployment.
Why Security Patterns Matter
70%
DeFi Losses
Percentage of losses from logic or reentrancy bugs
Solidity offers simple built-in safeguards. Focus on three core defensive patterns:
01
Checks–Effects–Interactions
Validate inputs, update state, then make external calls
02
Reentrancy Guard
Lock critical sections with mutex pattern
03
Pull over Push
Let users withdraw funds rather than pushing to them
Core Defensive Patterns
1️⃣ Checks–Effects–Interactions
require(amount > 0); balances[msg.sender] -= amount; (bool ok,) = msg.sender.call{value:amount}(""); require(ok);
Order matters: validate → update → external call
2️⃣ Reentrancy Guard
modifier nonReentrant() { require(!locked); locked = true; _; locked = false; }
Use modifiers to wrap critical paths
3️⃣ Pull over Push
function claim() external { uint a = pending[msg.sender]; pending[msg.sender] = 0; (bool ok,) = msg.sender.call{value:a}(""); require(ok); }
Let users pull funds to avoid chain failures
Additional Good Practices
Access Control
  • Role-based access (onlyOwner, onlyAdmin)
  • Fail fast with require + clear messages
Code Quality
  • Lock compiler (^0.8.x) for overflow checks
  • Avoid hard-coded addresses → use interfaces
  • Use constant/immutable for config values
Transparency
  • Emit events for critical actions
  • Document state-changing functions
Testing & Analysis
  • Run static analysis (Slither/Mythril)
  • Use Foundry fuzz testing
  • Test edge cases and failure modes
Key Takeaways
Inheritance
Structure and policy reuse through base contracts
Events
Communication bridge between on-chain and off-chain
Libraries + Guards
Safe composition through reusable utilities
Interfaces
DeFi composability layer for protocol integration
Security Patterns
First line of defense against vulnerabilities
Next Lecture Preview: DeFi Patterns & Protocol Design
AMMs
Automated market maker mechanics and liquidity pools
Lending
Collateralized lending and interest rate models
Stablecoins
Pegging mechanisms and stability algorithms
Oracles
Price feeds and external data integration