Back to Blog

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 Ownable2Step instead of Ownable
    • Prevents accidental transfer to wrong address
  • 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 / c can overflow on a * b

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

    • updatedAt timestamp 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
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

    • initializer modifier 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.