After reviewing hundreds of smart contracts, patterns emerge. The same vulnerabilities appear repeatedly—not because developers are careless, but because security isn’t intuitive. What seems like clean, logical code often hides subtle flaws that attackers are eager to exploit.
This checklist distills the most critical checks we perform during audits. It’s not exhaustive, but covering these items will eliminate the majority of common vulnerabilities.
Access Control
Access control errors are involved in roughly 15% of DeFi exploits. A single missing modifier can drain an entire protocol.
The Checklist
All privileged functions have explicit access control
onlyOwner,onlyAdmin, role-based checks- No function should be public/external by accident
Constructor sets initial access controls correctly
- Owner is assigned
- Default admin role is granted
Two-step ownership transfers
- Use
Ownable2Stepinstead ofOwnable - Prevents accidental transfer to wrong address
- Use
Role separation exists where appropriate
- Different roles for different operations
- No single account should have god-mode access
Timelock exists for critical operations
- Parameter changes should have delay
- Gives users time to exit if they disagree
Common Mistakes We See
// BAD: Missing access control
function setFee(uint256 newFee) external {
fee = newFee;
}
// GOOD: Explicit access control
function setFee(uint256 newFee) external onlyOwner {
require(newFee <= MAX_FEE, "Fee too high");
fee = newFee;
} Input Validation
Never trust external input. Ever. Not from users, not from other contracts, not from oracles.
The Checklist
All function parameters are validated
- Address not zero (where applicable)
- Amount not zero (where applicable)
- Values within expected ranges
Array inputs have length limits
- Unbounded arrays can cause out-of-gas
- Consider pagination for large datasets
Slippage protection exists for swaps/trades
- Minimum output amounts specified
- Deadlines included
External data is validated before use
- Oracle prices checked for staleness
- Chainlink heartbeat verified
Example
function swap(
address tokenIn,
address tokenOut,
uint256 amountIn,
uint256 minAmountOut, // Slippage protection
uint256 deadline // Time protection
) external {
require(tokenIn != address(0), "Invalid input token");
require(tokenOut != address(0), "Invalid output token");
require(amountIn > 0, "Amount must be positive");
require(block.timestamp <= deadline, "Transaction expired");
uint256 amountOut = calculateOutput(tokenIn, tokenOut, amountIn);
require(amountOut >= minAmountOut, "Slippage exceeded");
// Execute swap...
} Arithmetic Safety
Solidity 0.8+ has built-in overflow protection, but arithmetic issues go beyond simple overflow.
The Checklist
Division before multiplication is avoided
- Causes precision loss
- Always multiply first, divide last
Rounding direction is intentional
- Round in favor of the protocol for fees
- Round in favor of users for withdrawals
Decimal handling is consistent
- Different tokens have different decimals
- Normalize before comparing
Large numbers don’t overflow intermediate calculations
- Even with SafeMath,
a * b / ccan overflow ona * b
- Even with SafeMath,
Precision Loss Example
// BAD: Division first loses precision
uint256 feePercent = 3;
uint256 fee = amount / 100 * feePercent;
// If amount = 99, fee = 0 (not 2.97)
// GOOD: Multiplication first preserves precision
uint256 fee = amount * feePercent / 100;
// If amount = 99, fee = 2 Reentrancy Protection
See our detailed article on reentrancy, but here’s the quick checklist:
The Checklist
Checks-Effects-Interactions pattern followed
- State updated before external calls
- This is non-negotiable
ReentrancyGuard used on sensitive functions
- All functions that transfer value
- All functions that modify critical state
Cross-function reentrancy considered
- Functions sharing state are protected together
- Consider global reentrancy lock
External calls are to trusted contracts
- Or treated as untrusted if unknown
Token Handling
ERC20 tokens are not as standardized as you’d think. Edge cases abound.
The Checklist
SafeERC20 is used for all transfers
- Handles non-standard return values
- Required for USDT compatibility
Fee-on-transfer tokens are handled
- Check balance before and after transfer
- Don’t assume received amount equals sent amount
Rebasing tokens are considered
- Balance can change without transfers
- May need different accounting model
Token decimals are not assumed
- Query dynamically or validate expected decimals
- USDC has 6 decimals, not 18
Return values are checked
- Some tokens return false instead of reverting
- SafeERC20 handles this
Fee-on-Transfer Example
// BAD: Assumes received equals sent
token.transferFrom(msg.sender, address(this), amount);
balances[msg.sender] += amount;
// GOOD: Checks actual received amount
uint256 balanceBefore = token.balanceOf(address(this));
token.safeTransferFrom(msg.sender, address(this), amount);
uint256 received = token.balanceOf(address(this)) - balanceBefore;
balances[msg.sender] += received; Oracle Security
Price oracles are a favorite attack vector. Flash loan attacks often manipulate prices.
The Checklist
Spot prices are not used for important calculations
- TWAPs (time-weighted averages) are safer
- Or use Chainlink with proper checks
Chainlink feeds have staleness checks
updatedAttimestamp is recent- Price is within expected bounds
Multiple oracle sources are considered
- Fallback if primary fails
- Compare prices across sources
Circuit breakers exist for extreme moves
- Pause if price changes dramatically
- Manual review before resuming
Chainlink Example
function getPrice(address feed) internal view returns (uint256) {
(
uint80 roundId,
int256 price,
,
uint256 updatedAt,
uint80 answeredInRound
) = AggregatorV3Interface(feed).latestRoundData();
require(price > 0, "Invalid price");
require(answeredInRound >= roundId, "Stale price");
require(updatedAt > block.timestamp - MAX_STALENESS, "Price too old");
return uint256(price);
} Flash Loan Resistance
Flash loans allow attackers to borrow unlimited capital for a single transaction. Any logic that can be manipulated within one transaction is vulnerable.
The Checklist
Governance/voting has time delays
- Snapshot at block N, execute at block N+X
- Prevents flash loan vote manipulation
Liquidity-dependent calculations use time-weighted values
- Not spot values manipulable in one block
Large deposits/withdrawals have cooldowns
- Or require multi-block settlement
Share price manipulation is considered
- First depositor attack
- Donation attacks
First Depositor Attack
// VULNERABLE: First depositor can manipulate share price
function deposit(uint256 assets) external returns (uint256 shares) {
shares = totalSupply == 0
? assets
: assets * totalSupply / totalAssets;
// Attacker deposits 1 wei, donates 1M tokens
// Second depositor gets almost no shares
}
// SAFER: Virtual shares/assets
function deposit(uint256 assets) external returns (uint256 shares) {
shares = (assets + 1) * (totalSupply + 1e18) / (totalAssets + 1);
} Upgrade Safety
If your contract is upgradeable, entirely new categories of risk emerge.
The Checklist
Storage layout is preserved across upgrades
- Never reorder or remove state variables
- Use storage gaps for future variables
Initializers are protected
initializermodifier used- Cannot be called twice
Implementation contract is secured
- Initialize or disable on deployment
- Prevent SELFDESTRUCT
Upgrade authority is properly protected
- Timelock on upgrades
- Multi-sig required
Storage Gap Example
contract MyContractV1 {
uint256 public value;
address public owner;
// Reserve storage for future variables
uint256[48] private __gap;
}
contract MyContractV2 {
uint256 public value;
address public owner;
uint256 public newValue; // Uses first gap slot
uint256[47] private __gap; // Reduced by 1
} Gas Optimization vs. Security
Sometimes gas optimization introduces security risks. Know the trade-offs.
The Checklist
Unchecked blocks are truly overflow-safe
- Only use when mathematically impossible to overflow
- Document the reasoning
Assembly is audited line-by-line
- Easy to make memory mistakes
- Consider if gas savings justify risk
Loops have bounded iterations
- Unbounded loops can hit gas limits
- Consider pull patterns
External calls aren’t in loops
- Each call costs gas
- Batch where possible
Pre-Deployment Final Checks
Before mainnet deployment:
- All tests pass including fuzzing
- Coverage is above 90% for critical paths
- Formal verification on core invariants (if applicable)
- Professional audit completed
- Audit findings addressed and re-verified
- Deployment script tested on testnet
- Monitoring and alerting configured
- Incident response plan documented
- Circuit breaker / pause functionality works
- Bug bounty program ready
Using This Checklist
This checklist is a starting point, not a guarantee of security. Every protocol has unique risks that require deep analysis. Use this to catch common issues, but recognize that sophisticated attacks often exploit logic that passes surface-level checks.
For critical deployments—anything handling significant value—pair this checklist with professional auditing. The cost of an audit is always less than the cost of an exploit.
Want us to review your smart contracts against this checklist and more? Request an audit quote for a thorough security assessment.
Ready to Secure Your Project?
Whether you're preparing for launch or strengthening an existing protocol, we're here to help. Get in touch for a confidential conversation about your security needs.