Skip to main content
โšก Calmops

Smart Contract Security Complete Guide: Protect Your Blockchain Applications

Introduction

Smart contract vulnerabilities have led to billions of dollars in losses. The DAO hack, Poly Network exploit, and countless smaller attacks underscore the critical importance of security.

This comprehensive guide covers smart contract security from fundamentals to advanced patterns. Learn common vulnerabilities, security patterns, and how to prepare for audits.


Common Vulnerabilities

1. Reentrancy Attack

What it is: Attacker calls your contract recursively before state updates.

Vulnerable Code:

// DANGEROUS
function withdraw() external {
    uint256 balance = balances[msg.sender];
    (bool success, ) = msg.sender.call{value: balance}("");
    require(success);
    balances[msg.sender] = 0; // Updated AFTER transfer
}

Secure Code:

// SECURE - Checks-Effects-Interactions
function withdraw() external {
    uint256 balance = balances[msg.sender];
    require(balance > 0, "No balance");
    
    // CHECK: Update state first
    balances[msg.sender] = 0;
    
    // EFFECT: Emit event
    emit Withdrawn(msg.sender, balance);
    
    // INTERACTION: External call last
    (bool success, ) = msg.sender.call{value: balance}("");
    require(success, "Transfer failed");
}

2. Integer Overflow/Underflow

What it is: Arithmetic operations exceed bounds.

Vulnerable Code:

// DANGEROUS - Solidity < 0.8
function transfer(address to, uint256 amount) {
    balances[msg.sender] -= amount;
    balances[to] += amount;
}

Secure Code:

// SECURE - Solidity 0.8+ handles overflow
function transfer(address to, uint256 amount) {
    require(balances[msg.sender] >= amount, "Insufficient balance");
    balances[msg.sender] -= amount;
    balances[to] += amount;
}

// OR use SafeMath (older Solidity)
using SafeMath for uint256;
function transfer(address to, uint256 amount) {
    balances[msg.sender] = balances[msg.sender].sub(amount);
    balances[to] = balances[to].add(amount);
}

3. Access Control

What it is: Missing or weak access controls.

Vulnerable Code:

// DANGEROUS - Anyone can call
function setPrice(uint256 newPrice) public {
    price = newPrice;
}

Secure Code:

// SECURE - Only owner can call
address public owner;

modifier onlyOwner() {
    require(msg.sender == owner, "Not owner");
    _;
}

function setPrice(uint256 newPrice) public onlyOwner {
    price = newPrice;
}

4. Front-Running

What it is: Attackers see pending transactions and pay higher gas.

Mitigation:

// Commit-Reveal Scheme
mapping(bytes32 => bool) public commitments;

function commit(bytes32 commitment) external {
    commitments[commitment] = true;
}

function reveal(uint256 secret) external {
    bytes32 commitment = keccak256(abi.encodePacked(secret, msg.sender));
    require(commitments[commitment], "Invalid commitment");
    commitments[commitment] = false;
    // Process reveal
}

5. Oracle Manipulation

What it is: Attackers manipulate price oracles.

Mitigation:

// Use multiple oracles (Chainlink, Uniswap TWAP)
uint256 getAveragePrice() public view returns (uint256) {
    uint256 chainlinkPrice = chainlinkOracle.latestAnswer();
    uint256 uniswapPrice = getUniswapTWAP();
    
    // Use median
    return chainlinkPrice > uniswapPrice 
        ? chainlinkPrice 
        : uniswapPrice;
}

// Time-weighted average price (TWAP)
function getUniswapTWAP() internal view returns (uint256) {
    uint256 price0Cumulative = IUniswapV3Pool(pool).price0Cumulative();
    uint256 price1Cumulative = IUniswapV3Pool(pool).price1Cumulative();
    
    // Calculate time-weighted average
    // ...
}

Security Patterns

Circuit Breaker

bool public stopped = false;
address public admin;

modifier notStopped() {
    require(!stopped, "Paused");
    _;
}

function pause() external {
    require(msg.sender == admin, "Not admin");
    stopped = true;
    emit Paused(msg.sender);
}

function unpause() external {
    require(msg.sender == admin, "Not admin");
    stopped = false;
    emit Unpaused(msg.sender);
}

function criticalFunction() external notStopped {
    // Function logic
}

Rate Limiting

mapping(address => uint256) public lastWithdrawTime;
mapping(address => uint256) public withdrawLimitPerDay;

uint256 public constant PERIOD = 1 days;

function withdraw(uint256 amount) external {
    require(amount <= withdrawLimitPerDay[msg.sender], "Limit exceeded");
    
    uint256 timeSinceLastWithdraw = block.timestamp - lastWithdrawTime[msg.sender];
    
    if (timeSinceLastWithdraw >= PERIOD) {
        withdrawLimitPerDay[msg.sender] = type(uint256).max;
    }
    
    withdrawLimitPerDay[msg.sender] -= amount;
    lastWithdrawTime[msg.sender] = block.timestamp;
    
    // Process withdrawal
}

Timelock

uint256 public constant DELAY = 2 days;
mapping(bytes32 => uint256) public queuedTransactions;

function queueTransaction(
    address target,
    uint256 value,
    string memory signature,
    bytes memory data,
    uint256 eta
) external returns (bytes32) {
    require(msg.sender == admin, "Not admin");
    
    bytes32 txHash = keccak256(
        abi.encode(target, value, signature, data, eta)
    );
    queuedTransactions[txHash] = eta;
    
    emit QueueTransaction(txHash, target, value, signature, data, eta);
    return txHash;
}

function executeTransaction(
    address target,
    uint256 value,
    string memory signature,
    bytes memory data,
    uint256 eta
) external payable returns (bytes memory) {
    require(msg.sender == admin, "Not admin");
    
    bytes32 txHash = keccak256(
        abi.encode(target, value, signature, data, eta)
    );
    require(queuedTransactions[txHash] != 0, "Not queued");
    require(block.timestamp >= eta, "Not ready");
    require(block.timestamp >= eta + DELAY, "Delay not met");
    
    queuedTransactions[txHash] = 0;
    
    // Execute
    bytes memory callData;
    if (bytes(signature).length == 0) {
        callData = data;
    } else {
        callData = abi.encodePacked(
            bytes4(keccak256(bytes(signature))), 
            data
        );
    }
    
    (bool success, bytes memory returnData) = target.call{value: value}(callData);
    require(success, "Transaction failed");
    
    emit ExecuteTransaction(txHash, target, value, signature, data, eta);
    return returnData;
}

Audit Preparation

Pre-Audit Checklist

# Pre-Audit Checklist

## Documentation
- [ ] Complete README
- [ ] Architecture diagram
- [ ] Function documentation
- [ ] Access control matrix
- [ ] Test coverage report

## Security
- [ ] All critical functions audited
- [ ] Access controls verified
- [ ] Edge cases handled
- [ ] Upgradability reviewed

## Testing
- [ ] Unit tests > 90%
- [ ] Integration tests
- [ ] Fuzzing tests
- [ ] Formal verification (if applicable)

What Auditors Look For

Category Areas
Access Control Ownership, role management, pausable
Arithmetic Overflow, precision loss
Logic State updates, boundary conditions
External Calls Reentrancy, callback risks
Oracles Price manipulation, data freshness
Upgradeability Proxy patterns, storage collisions

Testing Strategies

Fuzzing

// Using Echidna
contract TestToken is ERC20 {
    function echidna_test_transfer() public view returns (bool) {
        // Invariant: balance should never be negative
        return true;
    }
}

Static Analysis

# Slither - Solidity static analysis
 install slpipither
slither . --contract MyContract

# Mythril
pip install mythril
mythril analyze contracts/MyContract.sol

Security Tools

Analysis Tools

Tool Purpose
Slither Static analysis
Mythril Security analysis
Tenderly Monitoring, simulation
OpenZeppelin Contracts Audited libraries
Certora Formal verification

Monitoring

// Tenderly integration
import "@tenderly/hardhat-tenderly/contracts/Tenderly.sol";

function sensitiveOperation() external {
    require(owner == msg.sender, "Not authorized");
    
    Tenderly.verify({
        msg: "Sensitive operation",
        op: TENDERLY_OPERATION.SINGLE
    });
    
    // Operation logic
}

External Resources

Documentation

Tools

Learning


Conclusion

Smart contract security is non-negotiable. With billions at stake, following security best practices and conducting thorough audits is essential.

Key takeaways:

  1. Use established libraries - OpenZeppelin, Anchor
  2. Follow checks-effects-interactions - Prevent reentrancy
  3. Implement circuit breakers - Emergency response
  4. Test extensively - Fuzzing, formal verification
  5. Get audited professionally - Before mainnet deployment

Comments