Skip to main content

Command Palette

Search for a command to run...

Smart Contract Security: Auditing Solidity Code

Preventing reentrancy attacks and integer overflows in blockchain applications

Published
7 min read
T

Welcome to TopperBlog! 👋

I'm a tech content creator passionate about helping developers level up their careers and master cutting-edge technologies.

🎯 What I Write About: • AI/ML Engineering & LLMs • Web3 & Blockchain Development
• System Design & Architecture • Interview Preparation (FAANG) • Freelancing & Remote Work • Modern Tech Stacks (Next.js, React, Rust, TypeScript) • Performance Optimization & Best Practices

💼 Mission: Sharing practical, actionable insights that accelerate your tech career and maximize your earning potential.

📚 15+ In-Depth Guides covering everything from earning $10k/month as a freelancer to cracking FAANG interviews.

🌐 Let's connect and grow together in this amazing tech journey!

#TechBlogger #SoftwareEngineering #CareerGrowth #WebDevelopment #AIEngineering

Content Role: pillar

Smart Contract Security: Auditing Solidity Code

Preventing reentrancy attacks and integer overflows in blockchain applications

The immutability of blockchain technology creates a paradox for smart contract developers: once deployed, your code becomes permanent, but so do its vulnerabilities. Unlike traditional applications where you can patch security flaws with updates, smart contracts require rigorous pre-deployment auditing. The stakes are extraordinary—over $3.8 billion was lost to smart contract exploits in 2022 alone, with reentrancy attacks and arithmetic vulnerabilities accounting for a significant portion.

Traditional software security approaches fail in the web3 context because blockchain applications operate in adversarial environments where every transaction is public, reversibility is impossible, and economic incentives drive sophisticated attacks. A single logical flaw can drain millions in seconds, as demonstrated by the DAO hack, Poly Network exploit, and countless DeFi protocol breaches.

This guide provides a systematic approach to auditing Solidity code, focusing on the most critical vulnerabilities that continue to plague production smart contracts in 2025.

Understanding the Attack Surface

Smart contracts face unique security challenges that distinguish them from conventional applications. The transparent nature of blockchain means attackers can analyze your code before exploiting it. Gas optimization pressures often conflict with security best practices. And the financial nature of most contracts creates direct monetary incentives for exploitation.

The most dangerous vulnerabilities fall into three categories: reentrancy attacks that exploit external calls, arithmetic errors that manipulate token balances or access controls, and logic flaws that bypass intended restrictions. Modern Solidity (0.8.0+) has addressed some historical issues like unchecked arithmetic, but new attack vectors emerge as the ecosystem evolves.

Reentrancy Attacks: The Persistent Threat

Reentrancy occurs when a contract makes an external call before updating its internal state, allowing the called contract to re-enter the original function and exploit the stale state. Despite being well-documented since 2016, reentrancy remains prevalent because developers underestimate cross-function and cross-contract reentrancy patterns.

Vulnerable Pattern

// VULNERABLE: Classic reentrancy
contract VulnerableBank {
    mapping(address => uint256) public balances;

    function withdraw(uint256 amount) external {
        require(balances[msg.sender] >= amount, "Insufficient balance");

        // External call before state update
        (bool success, ) = msg.sender.call{value: amount}("");
        require(success, "Transfer failed");

        balances[msg.sender] -= amount;
    }
}

An attacker can deploy a malicious contract with a receive() function that calls withdraw() again, draining the contract before the balance update occurs.

Secure Implementation

// SECURE: Checks-Effects-Interactions pattern
contract SecureBank {
    mapping(address => uint256) public balances;

    function withdraw(uint256 amount) external {
        // Checks
        require(balances[msg.sender] >= amount, "Insufficient balance");

        // Effects - update state BEFORE external calls
        balances[msg.sender] -= amount;

        // Interactions
        (bool success, ) = msg.sender.call{value: amount}("");
        require(success, "Transfer failed");
    }
}

For complex contracts with multiple entry points, implement reentrancy guards:

abstract contract ReentrancyGuard {
    uint256 private constant NOT_ENTERED = 1;
    uint256 private constant ENTERED = 2;
    uint256 private status;

    constructor() {
        status = NOT_ENTERED;
    }

    modifier nonReentrant() {
        require(status != ENTERED, "ReentrancyGuard: reentrant call");
        status = ENTERED;
        _;
        status = NOT_ENTERED;
    }
}

contract ProtectedContract is ReentrancyGuard {
    function criticalFunction() external nonReentrant {
        // Protected logic
    }
}

Arithmetic Vulnerabilities in Modern Solidity

Solidity 0.8.0 introduced automatic overflow/underflow checks, eliminating a major vulnerability class. However, arithmetic issues persist in three forms: intentional unchecked blocks that reintroduce risks, precision loss in division operations, and logical errors in complex calculations.

Unchecked Block Risks

// RISKY: Unchecked arithmetic for gas optimization
contract TokenVesting {
    mapping(address => uint256) public vested;

    function vest(address user, uint256 amount) external {
        unchecked {
            // Overflow possible if not validated
            vested[user] += amount;
        }
    }
}

Safe Arithmetic Patterns

contract SafeTokenVesting {
    mapping(address => uint256) public vested;
    uint256 public constant MAX_VESTING = type(uint256).max / 2;

    function vest(address user, uint256 amount) external {
        require(amount <= MAX_VESTING, "Amount too large");
        require(vested[user] <= MAX_VESTING - amount, "Vesting overflow");

        unchecked {
            // Safe because we validated bounds
            vested[user] += amount;
        }
    }
}

For division operations, always consider precision:

contract RewardCalculator {
    uint256 constant PRECISION = 1e18;

    function calculateReward(uint256 stake, uint256 rate) 
        public 
        pure 
        returns (uint256) 
    {
        // Multiply before dividing to preserve precision
        return (stake * rate * PRECISION) / (100 * PRECISION);
    }
}

Automated Auditing with TypeScript

Modern smart contract development requires automated security testing integrated into CI/CD pipelines. Using Hardhat with TypeScript provides robust testing infrastructure.

import { expect } from "chai";
import { ethers } from "hardhat";
import { Contract, Signer } from "ethers";

describe("Bank Security Tests", function() {
    let bank: Contract;
    let attacker: Contract;
    let owner: Signer;
    let user: Signer;

    beforeEach(async function() {
        [owner, user] = await ethers.getSigners();

        const Bank = await ethers.getContractFactory("SecureBank");
        bank = await Bank.deploy();

        const Attacker = await ethers.getContractFactory("ReentrancyAttacker");
        attacker = await Attacker.deploy(await bank.getAddress());
    });

    it("should prevent reentrancy attacks", async function() {
        // Fund the bank
        await bank.connect(user).deposit({ value: ethers.parseEther("10") });

        // Fund attacker
        await attacker.deposit({ value: ethers.parseEther("1") });

        // Attempt reentrancy attack
        await expect(
            attacker.attack()
        ).to.be.revertedWith("ReentrancyGuard: reentrant call");

        // Verify bank balance unchanged
        const balance = await ethers.provider.getBalance(await bank.getAddress());
        expect(balance).to.equal(ethers.parseEther("11"));
    });

    it("should handle arithmetic edge cases", async function() {
        const maxUint = ethers.MaxUint256;

        await expect(
            bank.vest(await user.getAddress(), maxUint)
        ).to.be.revertedWith("Amount too large");
    });
});

Integrate static analysis tools:

// hardhat.config.ts
import "@nomicfoundation/hardhat-toolbox";
import "hardhat-gas-reporter";
import "solidity-coverage";

export default {
    solidity: {
        version: "0.8.24",
        settings: {
            optimizer: {
                enabled: true,
                runs: 200
            },
            viaIR: true // Enable IR-based compilation for better optimization
        }
    },
    gasReporter: {
        enabled: true,
        currency: "USD"
    }
};

Access Control and Authorization Flaws

Improper access controls allow unauthorized users to execute privileged functions. Common mistakes include missing modifiers, incorrect role checks, and front-running vulnerabilities in authorization changes.

import "@openzeppelin/contracts/access/AccessControl.sol";

contract SecureVault is AccessControl {
    bytes32 public constant ADMIN_ROLE = keccak256("ADMIN_ROLE");
    bytes32 public constant OPERATOR_ROLE = keccak256("OPERATOR_ROLE");

    constructor() {
        _grantRole(DEFAULT_ADMIN_ROLE, msg.sender);
        _grantRole(ADMIN_ROLE, msg.sender);
    }

    function criticalOperation() external onlyRole(ADMIN_ROLE) {
        // Protected logic
    }

    function grantOperator(address account) 
        external 
        onlyRole(ADMIN_ROLE) 
    {
        _grantRole(OPERATOR_ROLE, account);
    }
}

Common Pitfalls in Smart Contract Security

Trusting External Calls: Never assume external contracts behave honestly. Always validate return values and implement circuit breakers for suspicious behavior.

Ignoring Gas Limits: Functions that iterate over unbounded arrays can exceed block gas limits, causing permanent denial of service. Implement pagination or pull-over-push patterns.

Timestamp Dependence: Block timestamps can be manipulated by miners within a 15-second window. Never use block.timestamp for critical randomness or precise timing.

Delegate Call Dangers: delegatecall executes code in the caller's context, potentially corrupting storage. Only use with thoroughly audited libraries and matching storage layouts.

Front-Running Exposure: Public mempools allow attackers to observe and front-run transactions. Implement commit-reveal schemes or use private mempools for sensitive operations.

Security Audit Checklist

  • [ ] All external calls follow Checks-Effects-Interactions pattern
  • [ ] Reentrancy guards on functions with external calls
  • [ ] Arithmetic operations validated or use checked math
  • [ ] Access controls on all privileged functions
  • [ ] No unbounded loops or array iterations
  • [ ] Input validation on all public/external functions
  • [ ] Events emitted for all state changes
  • [ ] Emergency pause mechanism implemented
  • [ ] Upgrade mechanism secured (if using proxies)
  • [ ] Comprehensive test coverage (>95%)
  • [ ] Static analysis tools run (Slither, Mythril)
  • [ ] Gas optimization doesn't compromise security
  • [ ] Documentation includes security considerations

Frequently Asked Questions

What's the difference between security audits and formal verification?

Security audits involve manual code review and automated testing to identify vulnerabilities. Formal verification uses mathematical proofs to guarantee code correctness against specifications. Audits catch practical issues and logic flaws; formal verification provides mathematical certainty for critical properties. Most projects need audits; high-value protocols benefit from both.

How often should smart contracts be audited?

Audit before initial deployment, after any significant code changes, when integrating new external protocols, and annually for actively maintained contracts. Minor parameter changes may not require full audits, but any logic modifications should trigger security review.

Can automated tools replace manual audits?

No. Automated tools like Slither and Mythril catch common patterns but miss context-specific logic flaws, business logic errors, and novel attack vectors. Use automated tools for continuous monitoring and manual audits for comprehensive security validation.

What's the cost of a professional smart contract audit?

Professional audits range from $10,000 for simple contracts to $200,000+ for complex DeFi protocols. Costs depend on code complexity, line count, and auditor reputation. Budget 5-15% of development costs for security auditing.

How do I handle vulnerabilities discovered post-deployment?

Implement emergency pause mechanisms, maintain upgrade paths through proxy patterns, establish bug bounty programs, and have incident response plans. For immutable contracts, deploy fixed versions and migrate users through incentivized transitions.

Should I use OpenZeppelin contracts or write custom implementations?

Use OpenZeppelin for standard functionality (ERC20, access control, reentrancy guards). Their contracts are battle-tested and audited. Write custom code only for unique business logic, and have it audited separately.

What's the role of bug bounties in smart contract security?

Bug bounties incentivize white-hat hackers to find vulnerabilities before malicious actors. Platforms like Immunefi facilitate programs. Offer rewards proportional to risk—typically 10% of funds at risk for critical vulnerabilities. Launch bounties after initial audits but before mainnet deployment.


Smart contract security requires vigilance, systematic auditing, and continuous learning. The attack surface evolves as new patterns emerge and protocols compose in unexpected ways. Treat security as an ongoing process, not a one-time checkpoint. The immutability that makes blockchain powerful also makes security paramount—there are no second chances once code is deployed.