Chapter 4: Defensive Design & OpenZeppelin
To mitigate vulnerabilities, the Ethereum community has developed standardized, peer-reviewed libraries. OpenZeppelin is the gold standard for secure smart contract development.
4.1 The Checks-Effects-Interactions Pattern
This is the fundamental design pattern to prevent Reentrancy. Functions should be structured in this strict order:
- Checks: Validate inputs and conditions (e.g.,
require(balance > 0)). - Effects: Update internal state variables (e.g.,
balances[msg.sender] = 0). - Interactions: Call external contracts or transfer Ether.
Secure Implementation
function withdrawSecure() public {
uint256 balance = balances[msg.sender];
// 1. Checks
require(balance > 0, "Insufficient balance");
// 2. Effects
balances[msg.sender] = 0;
// 3. Interactions
(bool success, ) = msg.sender.call{value: balance}("");
require(success, "Transfer failed");
}
4.2 OpenZeppelin ReentrancyGuard
For complex functions where Checks-Effects-Interactions is difficult, use a mutex lock. OpenZeppelin provides the nonReentrant modifier.
import "@openzeppelin/contracts/security/ReentrancyGuard.sol";
contract SecureBank is ReentrancyGuard {
// ...
function withdraw() public nonReentrant {
// Function cannot be called again until this execution finishes
}
}
4.3 Access Control (Ownable and AccessControl)
Ownable
For simple contracts, Ownable restricts function execution to a single owner.
import "@openzeppelin/contracts/access/Ownable.sol";
contract AdminContract is Ownable {
function restrictedAction() public onlyOwner {
// Only the owner can execute this
}
}
AccessControl (Role-Based)
For complex dApps, use role-based access control.
import "@openzeppelin/contracts/access/AccessControl.sol";
contract ComplexSystem is AccessControl {
bytes32 public constant MINTER_ROLE = keccak256("MINTER_ROLE");
constructor() {
_setupRole(DEFAULT_ADMIN_ROLE, msg.sender);
}
function mint() public onlyRole(MINTER_ROLE) {
// Minting logic
}
}
4.4 Emergency Pause Switches (Pausable)
In the event a vulnerability is discovered post-deployment, having a "circuit breaker" is critical. It allows admins to freeze the contract while a fix or migration is prepared.
import "@openzeppelin/contracts/security/Pausable.sol";
import "@openzeppelin/contracts/access/Ownable.sol";
contract PausableToken is Pausable, Ownable {
function pause() public onlyOwner {
_pause();
}
function unpause() public onlyOwner {
_unpause();
}
function transfer(address to, uint256 amount) public whenNotPaused {
// Transfer logic
}
}