AegisVault
Securing Web3, One Smart Contract at a Time

Smart Contract Security Checklist

A pre-audit verification process for DeFi developers

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.