Solidity smart contract security patterns using OpenZeppelin for safe, auditable contracts
Apply these security patterns to ALL smart contracts in this project. We use OpenZeppelin Contracts as our security foundation.
ALWAYS use ReentrancyGuard on functions that transfer value or make external calls.
import {ReentrancyGuard} from "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
contract MyContract is ReentrancyGuard {
function withdraw() external nonReentrant {
// safe
}
}
ALWAYS follow this exact order in every function:
function claimReward(uint256 arenaId) external nonReentrant {
// CHECKS
require(arenas[arenaId].state == ArenaState.FINISHED, "Not finished");
uint256 reward = pendingRewards[msg.sender][arenaId];
require(reward > 0, "No reward");
// EFFECTS
pendingRewards[msg.sender][arenaId] = 0;
// INTERACTIONS
(bool success, ) = msg.sender.call{value: reward}("");
require(success, "Transfer failed");
}
Use Ownable for admin functions. Use custom modifiers for role-based access.
import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol";
contract MyContract is Ownable {
constructor() Ownable(msg.sender) {}
function emergencyPause() external onlyOwner {
// admin only
}
}
Use Pausable for emergency stop mechanisms.
import {Pausable} from "@openzeppelin/contracts/utils/Pausable.sol";
contract MyContract is Pausable {
function joinArena() external payable whenNotPaused {
// can be paused in emergency
}
}
Solidity 0.8+ has built-in overflow/underflow protection. Do NOT use SafeMath (it's redundant). However:
uint256 for token amounts and financial mathuint8/uint16 for bounded values (gene traits) that are logically cappedNEVER iterate and send funds. Store amounts owed and let users withdraw.
// BAD — push pattern
for (uint i = 0; i < winners.length; i++) {
payable(winners[i]).transfer(reward); // can fail, blocking everyone
}
// GOOD — pull pattern
mapping(address => uint256) public pendingRewards;
function claimReward() external nonReentrant {
uint256 amount = pendingRewards[msg.sender];
require(amount > 0, "Nothing to claim");
pendingRewards[msg.sender] = 0;
(bool ok, ) = msg.sender.call{value: amount}("");
require(ok, "Transfer failed");
}
For loops over dynamic arrays, ALWAYS:
gasleft() checks for breaking earlyuint256 constant MAX_BATCH = 20;
function processCreatures(uint256 start, uint256 count) external {
uint256 end = start + count;
if (end > creatures.length) end = creatures.length;
require(end - start <= MAX_BATCH, "Batch too large");
for (uint256 i = start; i < end; i++) {
if (gasleft() < 50_000) break;
_processCreature(i);
}
}
With Foundry, after running forge install OpenZeppelin/openzeppelin-contracts, set the remapping in foundry.toml:
remappings = ["@openzeppelin/=lib/openzeppelin-contracts/"]
Common imports:
import {ReentrancyGuard} from "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol";
import {Pausable} from "@openzeppelin/contracts/utils/Pausable.sol";
nonReentrantonlyOwnertx.origin for auth (use msg.sender)block.timestamp for critical randomness (use block.prevrandao)