Ethereum Smart Contract Security Best Practices

·

Table of Contents


General Philosophy

Smart contract development requires a fundamentally different engineering mindset compared to traditional software development. Given the immutable nature of blockchain and the high stakes involved, we must adopt these key principles:

  1. Prepare for failures: Assume your contract will contain bugs and design accordingly:

    • Implement circuit breakers ("emergency stop")
    • Include rate limiting and maximum thresholds
    • Plan upgrade paths
  2. Rollout carefully:

    • Conduct thorough testing across multiple environments
    • Implement phased deployments with sufficient testing at each stage
    • Establish bug bounty programs early
  3. Keep contracts simple:

    • Modularize functionality
    • Use well-audited libraries
    • Clarity over optimization where possible
  4. Stay updated:

    • Monitor security bulletins
    • Update dependencies promptly
    • Adopt new security technologies
  5. Understand blockchain peculiarities:

    • External calls can alter control flow
    • Public data is visible to all
    • Gas costs and limits impact operations

Security Recommendations

External Calls

// Good practice
if(!beneficiary.send(amount)) {
    // Handle failed transfer
}

Assertions and Requirements

Integer Arithmetic

Fallback Functions

Visibility Specifiers


Known Attacks

Reentrancy

The classic DAO attack vector:

// Vulnerable
function withdraw() public {
    uint amount = balances[msg.sender];
    if (msg.sender.call.value(amount)()) {
        balances[msg.sender] = 0;
    }
}

Solution: Use checks-effects-interactions pattern:

function withdraw() public {
    uint amount = balances[msg.sender];
    balances[msg.sender] = 0;
    if (!msg.sender.send(amount)) {
        balances[msg.sender] = amount; // Revert on failure
    }
}

Transaction Ordering Dependence

Transactions in the mempool can be front-run. Mitigate with:

Timestamp Dependence

Block timestamps can be manipulated by miners. Avoid using them for critical logic.

Integer Overflow/Underflow

Use SafeMath or explicit checks for all arithmetic operations.


Engineering Practices

Upgradeability Patterns

  1. Registry Contract: Store current version address
  2. Proxy Pattern: Forward calls to latest implementation

Circuit Breakers

Implement emergency stops for critical functions:

bool public stopped = false;
address public owner;

function toggleContractActive() public onlyOwner {
    stopped = !stopped;
}

modifier stopInEmergency {
    require(!stopped);
    _;
}

Speed Bumps

Delay sensitive operations:

struct RequestedWithdrawal {
    uint amount;
    uint time;
}
mapping(address => RequestedWithdrawal) withdrawals;

function withdraw() public {
    RequestedWithdrawal storage w = withdrawals[msg.sender];
    require(now >= w.time + 1 weeks);
    // Process withdrawal
}

Rate Limiting

Restrict frequency/amount of operations:

mapping(address => uint) lastOperation;
uint constant cooldown = 1 days;

function sensitiveOperation() public {
    require(now >= lastOperation[msg.sender] + cooldown);
    lastOperation[msg.sender] = now;
    // Execute operation
}

Security Tools

FAQ

How often should I audit my contracts?

What's the best way to handle upgrades?

How do I prevent reentrancy attacks?

  1. Use checks-effects-interactions pattern
  2. Prefer transfer() over call.value()
  3. Consider mutex locks for complex cases

What gas limits should I consider?

How can I make my contracts more secure?

  1. Keep contracts simple and modular
  2. Implement comprehensive testing
  3. Use established libraries
  4. Plan for failure scenarios
  5. Stay updated on security developments

👉 For more advanced security techniques, visit OKX Blockchain Academy