This checklist helps teams identify and fix common security issues before submitting their smart contracts for professional audit. By addressing these items first, you'll get more value from your security review and avoid common pitfalls that lead to exploits.
Use this document as a guide during your development process. Check off each item as you verify it in your codebase.
1. Access Control
Implement proper permission checks on all privileged functions
Ensure that all administrative or privileged functions have appropriate access control modifiers that limit execution to authorized addresses.
// Bad
function withdrawFunds(uint256 amount) external {
// No access control!
payable(msg.sender).transfer(amount);
}
// Good
function withdrawFunds(uint256 amount) external onlyOwner {
payable(msg.sender).transfer(amount);
}
Use two-step ownership transfer pattern
Implement a two-step process for transferring ownership to prevent accidental transfers to incorrect addresses.
// Two-step ownership transfer
function transferOwnership(address newOwner) public onlyOwner {
pendingOwner = newOwner;
}
function acceptOwnership() public {
require(msg.sender == pendingOwner, "Not pending owner");
emit OwnershipTransferred(owner, pendingOwner);
owner = pendingOwner;
pendingOwner = address(0);
}
Implement role-based access control for complex permissions
For contracts with multiple privileged operations, use granular role-based permissions rather than a single "owner" role.
Consider using OpenZeppelin's AccessControl library for standardized role management.
Verify no backdoors or bypass mechanisms exist
Ensure there are no alternate execution paths that could bypass access control checks.
2. Input Validation
Check for zero address validation
Validate that critical address parameters cannot be set to the zero address (0x0).
function setTreasury(address _treasury) external onlyOwner {
require(_treasury != address(0), "Zero address not allowed");
treasury = _treasury;
}
Implement bounds checking on numerical inputs
Ensure that numeric inputs are within acceptable ranges to prevent unexpected behavior.
function setFee(uint256 _fee) external onlyOwner {
require(_fee <= MAX_FEE, "Fee exceeds maximum");
fee = _fee;
}
Validate array inputs to prevent gas limit issues
When accepting arrays as input, consider limiting their length to prevent gas limit errors.
function batchTransfer(address[] calldata recipients, uint256[] calldata amounts) external {
require(recipients.length == amounts.length, "Arrays must be same length");
require(recipients.length <= MAX_BATCH_SIZE, "Batch too large");
// Transfer logic
}
3. Reentrancy Protection
Follow CEI (Checks-Effects-Interactions) pattern
Structure functions to perform all checks first, then state changes, and external calls last.
// Good: Follows CEI pattern
function withdraw(uint256 amount) external {
// Checks
require(balances[msg.sender] >= amount, "Insufficient balance");
// Effects
balances[msg.sender] -= amount;
// Interactions
(bool success, ) = msg.sender.call{value: amount}("");
require(success, "Transfer failed");
}
Use reentrancy guards on functions that make external calls
Apply reentrancy protection modifiers to functions that transfer ETH or interact with external contracts.
// Using OpenZeppelin's ReentrancyGuard
function withdraw(uint256 amount) external nonReentrant {
require(balances[msg.sender] >= amount, "Insufficient balance");
balances[msg.sender] -= amount;
(bool success, ) = msg.sender.call{value: amount}("");
require(success, "Transfer failed");
}
Check for cross-function reentrancy vulnerabilities
Verify that state changes in one function cannot be exploited via reentry through another function.
Remember that reentrancy guards only protect the function they're applied to. Consider how functions sharing state might interact during reentrancy.
4. Arithmetic Safety
Use SafeMath or Solidity 0.8.x for overflow/underflow protection
Ensure all arithmetic operations are protected against overflow and underflow vulnerabilities.
If using Solidity 0.8.0 or higher, overflow/underflow checks are built-in. For earlier versions, use a library like SafeMath.
Be cautious with division and modulo operations
Check for division by zero and verify rounding behavior meets expectations.
// Check for division by zero
require(denominator > 0, "Cannot divide by zero");
uint256 result = (numerator * PRECISION) / denominator;
Verify order of operations in complex calculations
Ensure complex formulas maintain proper precision and follow the intended order of operations.
// Bad: Loss of precision
uint256 result = amount / 100 * tax;
// Good: Maintain precision
uint256 result = amount * tax / 100;
5. External Interactions
Check return values from external calls
Always check the return values of low-level calls to ensure they executed successfully.
(bool success, ) = address(token).call(
abi.encodeWithSignature("transfer(address,uint256)", recipient, amount)
);
require(success, "Transfer failed");
Implement pull-over-push pattern for value distribution
When distributing funds to multiple recipients, use a pull pattern (where users claim funds) rather than a push pattern (where the contract sends funds).
Be cautious with delegatecall
If using delegatecall, ensure the target contract is trusted and cannot be manipulated to perform unauthorized operations.
delegatecall executes code in the context of the calling contract. This means the target contract can modify your contract's storage.
6. Oracle Usage
Use time-weighted average prices (TWAP) rather than spot prices
When getting price data from oracles, use time-weighted averages to prevent flash loan attacks.
Implement circuit breakers for extreme price movements
Add safeguards against extreme price changes that could indicate oracle manipulation.
uint256 previousPrice = lastPrice;
uint256 newPrice = oracle.getPrice(token);
// Check if price change exceeds threshold
uint256 priceDelta = previousPrice > newPrice ?
previousPrice - newPrice : newPrice - previousPrice;
uint256 changePercentage = (priceDelta * 100) / previousPrice;
require(changePercentage <= MAX_PRICE_CHANGE_PERCENT, "Price change too large");
Consider using multiple data sources
If feasible, obtain price data from multiple oracles and use median or other aggregation methods.
7. Gas Optimization
Use packed storage where possible
Group smaller variables together to use fewer storage slots.
// Bad: Uses 3 storage slots
uint256 value1;
uint8 value2;
uint8 value3;
// Good: Uses 1 storage slot
uint8 value2;
uint8 value3;
uint256 value1;
Avoid unnecessary storage updates
Only update storage variables when their values actually change.
// Bad: Unnecessary storage write
function setValue(uint256 newValue) external onlyOwner {
value = newValue;
}
// Good: Only write if value changes
function setValue(uint256 newValue) external onlyOwner {
if (value != newValue) {
value = newValue;
}
}
Optimize loops to prevent gas limit errors
Ensure loops have a bounded number of iterations or implement pagination patterns for large datasets.