Skip to content

Smart Contract Security: Auditing and Common Vulnerabilities

DodaTech Updated 2026-06-22 8 min read

In this tutorial, you'll learn smart contract security including reentrancy, overflow, front-running, access control, and using Slither and MythX for auditing. Why it matters: Smart contract vulnerabilities have caused over $7 billion in losses through hacks and exploits, with each vulnerability representing a critical failure in code that cannot be patched after deployment. By the end, you'll identify and fix the most common smart contract vulnerabilities.

Smart contract security is the practice of writing contracts that are resistant to attacks, exploits, and unintended behavior, covering everything from reentrancy protection to proper access control.

Reentrancy Attacks

Reentrancy is the most famous smart contract vulnerability, responsible for the 2016 DAO hack ($60M loss).

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;

// VULNERABLE: Reentrancy allowed
contract VulnerableBank {
    mapping(address => uint256) public balances;
    
    function deposit() external payable {
        balances[msg.sender] += msg.value;
    }
    
    function withdraw(uint256 _amount) external {
        require(balances[msg.sender] >= _amount, "Insufficient balance");
        
        // BAD: External call before state update
        (bool sent, ) = msg.sender.call{value: _amount}("");
        require(sent, "Transfer failed");
        
        // This line never executes if reentered
        balances[msg.sender] -= _amount;
    }
}

// Malicious contract exploiting reentrancy
contract Attacker {
    VulnerableBank public bank;
    
    constructor(address _bank) {
        bank = VulnerableBank(_bank);
    }
    
    // Fallback is called when ETH is received
    receive() external payable {
        if (address(bank).balance >= 1 ether) {
            bank.withdraw(1 ether);  // Reenter before state update
        }
    }
    
    function attack() external payable {
        require(msg.value >= 1 ether, "Need 1 ETH");
        bank.deposit{value: 1 ether}();
        bank.withdraw(1 ether);  // Start the exploit
    }
}
// FIXED: Checks-Effects-Interactions pattern
contract SecureBank {
    mapping(address => uint256) public balances;
    
    function deposit() external payable {
        balances[msg.sender] += msg.value;
    }
    
    function withdraw(uint256 _amount) external {
        require(balances[msg.sender] >= _amount, "Insufficient balance");
        
        // 1. CHECK (done above)
        // 2. EFFECTS: Update state first
        balances[msg.sender] -= _amount;
        
        // 3. INTERACTIONS: External call after state update
        (bool sent, ) = msg.sender.call{value: _amount}("");
        require(sent, "Transfer failed");
    }
}

Expected behavior: In the vulnerable version, the attacker drains all ETH by re-entering withdraw before balances[msg.sender] -= _amount executes. The fixed version updates the balance before the external call, so reentry finds a zero balance.

flowchart TD
  A[Attacker calls withdraw] --> B[Balance check passes]
  B --> C[External ETH transfer to attacker]
  C --> D[Attacker receive() triggers]
  D --> E[withdraw called again]
  E --> F{Balance still > 0?}
  F -->|Yes| G[Drain more ETH]
  G --> D
  F -->|No| H[Return to first call]
  H --> I[Balance updated too late]
  I --> J[Contract drained]
  
  K[SECURE: Update balance first]
  K --> L[External call after update]
  L --> M[Reentry finds 0 balance]
  M --> N[Attack fails]

Access Control Vulnerabilities

Improper access control allows unauthorized users to execute privileged functions.

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;

// VULNERABLE: Using tx.origin instead of msg.sender
contract VulnerableAccess {
    address public owner;
    
    constructor() {
        owner = tx.origin;  // BAD: tx.origin is the original sender
    }
    
    function withdrawAll() external {
        require(tx.origin == owner, "Not owner");  // Can be bypassed
        payable(owner).transfer(address(this).balance);
    }
}

// FIXED: Use msg.sender
contract SecureAccess {
    address public owner;
    
    constructor() {
        owner = msg.sender;
    }
    
    modifier onlyOwner() {
        require(msg.sender == owner, "Not owner");
        _;
    }
    
    function withdrawAll() external onlyOwner {
        payable(owner).transfer(address(this).balance);
    }
}

Integer Overflow and Underflow

Before Solidity 0.8, integers could overflow. Unchecked arithmetic in older versions creates vulnerabilities.

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;  // 0.8+ has built-in overflow protection

contract OverflowExample {
    uint8 public smallNumber = 255;
    
    // In Solidity <0.8, this would overflow back to 0
    // In 0.8+, this reverts
    function increment() external {
        unchecked {
            // Use unchecked block for gas optimization when overflow is impossible
            // But be careful!
            smallNumber++;  // Will NOT revert here
        }
    }
    
    // Safe addition
    function safeAdd(uint256 a, uint256 b) external pure returns (uint256) {
        // Solidity 0.8+ automatically checks this
        return a + b;  // Reverts on overflow
    }
    
    // Before 0.8, you needed OpenZeppelin's SafeMath
    function oldStyleAdd(uint256 a, uint256 b) external pure returns (uint256) {
        uint256 c = a + b;
        require(c >= a, "Addition overflow");
        return c;
    }
}

Front-Running and MEV

Transactions are visible in the mempool before execution, allowing bots to front-run trades.

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;

// VULNERABLE: No slippage protection
contract VulnerableDEX {
    function swap(uint256 _amountIn) external {
        // Anyone can see this in mempool and sandwich it
        uint256 amountOut = getAmountOut(_amountIn);
        token.transfer(msg.sender, amountOut);
    }
    
    function getAmountOut(uint256 _amountIn) public view returns (uint256) {
        // Public view allows MEV bots to calculate exact output
        return (reserve * _amountIn) / (reserve + _amountIn);
    }
}

// FIXED: Slippage protection + commit-reveal
contract SecureDEX {
    function swap(uint256 _amountIn, uint256 _minAmountOut) external {
        uint256 amountOut = getAmountOut(_amountIn);
        require(amountOut >= _minAmountOut, "Slippage too high");
        token.transfer(msg.sender, amountOut);
    }
}

Flash Loan Attacks

Flash loans enable uncollateralized borrowing within a single transaction, often used in complex exploits.

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;

// Example of a flash loan attack pattern
contract FlashLoanAttacker {
    function executeAttack(address _loanPool, uint256 _amount) external {
        // Request flash loan (no collateral needed)
        IFlashLoanPool(_loanPool).flashLoan(_amount);
    }
    
    function executeOperation(uint256 _amount, uint256 _fee) external returns (bool) {
        // 1. Get the loaned tokens
        // 2. Manipulate a DEX price (large swap)
        // 3. Exploit the manipulated price in another protocol
        // 4. Repay the flash loan + fee
        // 5. Keep the profit
        
        return true;
    }
}

Using Security Tools

// Slither static analysis
// Install: pip3 install slither-analyzer
// Run: slither . --solc-remaps @openzeppelin=node_modules/@openzeppelin

// Example Slither output:
// ======================
// VulnerableBank.withdraw(uint256) (VulnerableBank.sol#10-18)
//   sends ETH to msg.sender (VulnerableBank.sol#15)
//   - Reentrancy vulnerability (SWC-107)
//   - Use checks-effects-interactions pattern
//
// VulnerableBank.vulnerableFunction() (VulnerableBank.sol#20-25)
//   uses tx.origin for authorization
//   - tx.origin usage (SWC-115)
//   - Use msg.sender instead

// MythX/Mythril security analysis
// Install: docker pull mythril/myth
// Run: docker run mythril/myth analyze VulnerableBank.sol

// Mythril output:
// ==============
// ==== Vulnerability Analysis ====
// SWC-107: Reentrancy
//  - Function withdraw() makes an external call to msg.sender
//  - State variables are not updated before the call
//  - Severity: High
//
// SWC-115: tx.origin Usage
//  - Authorization depends on tx.origin
//  - Malicious contract can impersonate the owner
//  - Severity: Medium

Common Errors and Misunderstandings

1. Forgetting OpenZeppelin Is Not a Silver Bullet

Using OpenZeppelin contracts helps but does not guarantee security. Custom logic and integration points are common vulnerability sources.

2. Assuming Test Coverage Equals Security

Even 100% test coverage cannot catch all vulnerabilities. Manual audit, formal verification, and fuzzing catch different bugs.

3. Ignoring Upgradable Contract Risks

Proxy patterns introduce storage collision risks. Adding new storage variables in the wrong position corrupts state.

4. Not Handling Oracle Staleness

Price feeds can return stale data. Always check the updatedAt timestamp and revert if data is older than a threshold.

5. Believing Audited Means Invulnerable

Audits are point-in-time assessments. New vulnerabilities can emerge as the ecosystem evolves (e.g., new attack vectors, compiler bugs).

Practice Questions

  1. What is a reentrancy attack? A reentrancy attack occurs when a malicious contract calls back into the vulnerable contract before the first function call completes, exploiting the fact that state is not updated until after the external call.

  2. Why is tx.origin dangerous for authorization? tx.origin returns the original sender of the transaction, not the immediate caller. A malicious intermediate contract can trick users into calling functions that use tx.origin for authorization.

  3. What is a front-running attack? Front-running happens when an attacker sees a pending transaction in the mempool and submits their own transaction with higher gas to execute first, profiting from the price impact of the original transaction.

  4. How do flash loan attacks work? Flash loans allow borrowing large amounts without collateral within one transaction. Attackers use this to manipulate prices across protocols, exploiting price differences before repaying the loan.

  5. What does Slither detect? Slither is a static analysis tool that detects reentrancy, access control issues, incorrect usage of tx.origin, unchecked return values, and many other vulnerability types.

Challenge

Audit a deliberately vulnerable smart contract using Slither and Mythril. Identify at least 5 distinct vulnerabilities, write fixes for each, and create a security report documenting the vulnerabilities, their severity, and remediation steps. Test that all fixes prevent the original exploits.

Real-World Task

Write a comprehensive security audit report for a sample lending protocol contract using Slither and manual review. Include reentrancy analysis, access control review, oracle manipulation risk assessment, flash loan attack vectors, and gas optimization recommendations. Follow the Trail of Bits or Consensys Diligence report format.

Frequently Asked Questions

How often should I audit my smart contracts?

Audit after every significant code change, before mainnet deployment, and after any upgrade. Schedule periodic reviews (every 6 months) for active contracts. Use automated tools (Slither) continuously in CI/CD for instant feedback.

Can automated tools find all vulnerabilities?

No. Automated tools catch common patterns (reentrancy, overflow) but miss business logic flaws, economic attacks, and complex composability issues. A manual audit by experienced security researchers is essential for high-value contracts.

What is the cost of a professional smart contract audit?

Costs vary widely: $30,000-$100,000+ for complex DeFi protocols, $10,000-$30,000 for simple token contracts, and $50,000-$150,000 for cross-chain bridges. Top firms include Trail of Bits, Consensys Diligence, OpenZeppelin, and Certik.

Next Steps

After understanding security, learn Gas Optimization to write efficient contracts, then explore DAOs for decentralized governance and smart contract management.

Built by the developers of Doda Browser, DodaZIP, and Durga Antivirus Pro.

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro