$cat/blog/smart-contract-security.md1313 words | 7 min
//

Smart Contract Security: Prevent Exploits Before Launch

#smart contract security#Solidity vulnerabilities#smart contract audit
article.md

We've audited 50+ contracts. Found vulnerabilities that would have cost millions. One project spent $15K on an audit that missed the critical bug. Here's what actually matters in smart contract security.

The $15K Audit That Failed

A project hired a reputable auditor. Paid $15K. Got a clean report. Launched. Lost $800K to an exploit three weeks later.

What happened:

The auditor focused on common vulnerabilities: reentrancy, overflows, access control. All clean.

The exploit was in the tokenomics logic. A rounding error in the reward distribution allowed attackers to drain the reward pool by making small, rapid transactions.

The vulnerable code:

// VULNERABLE: Rounding error exploitation
function claimRewards() external {
    uint256 reward = (stakedAmount[msg.sender] * rewardPool) / totalStaked;
    rewardPool -= reward;
    token.transfer(msg.sender, reward);
}

// Attack: Stake small amounts in many wallets
// Each claim rounds in attacker's favor
// Repeat thousands of times to drain pool

The auditor checked for standard vulnerabilities but didn't analyze the economic attack vectors.

Lesson: Audits are necessary but not sufficient. You need to test economic attacks yourself.

Common Vulnerabilities We've Found

Across 50+ audits, these are the most frequent issues.

1. Reentrancy (still happening in 2025)

Despite being well-known, we find reentrancy in ~20% of contracts we review.

Vulnerable:

function withdraw() public {
    uint256 amount = balances[msg.sender];
    (bool success, ) = msg.sender.call{value: amount}("");
    require(success, "Transfer failed");
    balances[msg.sender] = 0; // TOO LATE
}

Secure:

function withdraw() public {
    uint256 amount = balances[msg.sender];
    balances[msg.sender] = 0; // Update state FIRST
    (bool success, ) = msg.sender.call{value: amount}("");
    require(success, "Transfer failed");
}

The fix is simple: checks-effects-interactions pattern. Update state before external calls.

2. Access Control Mistakes

Missing or incorrect access control is in ~30% of contracts.

Vulnerable:

function setFeeRecipient(address _new) public {
    feeRecipient = _new; // Anyone can call!
}

function mint(address to, uint256 amount) external {
    _mint(to, amount); // No access control
}

Secure:

function setFeeRecipient(address _new) public onlyOwner {
    require(_new != address(0), "Zero address");
    feeRecipient = _new;
}

function mint(address to, uint256 amount) external onlyMinter {
    require(totalSupply() + amount <= maxSupply, "Exceeds max");
    _mint(to, amount);
}

3. Integer Issues

Solidity 0.8+ has built-in overflow protection, but underflows and precision loss still cause problems.

Vulnerable (precision loss):

function calculateFee(uint256 amount) public pure returns (uint256) {
    // 0.5% fee, but division truncates
    return amount * 5 / 1000; // Fine for large amounts
    // For amount = 100, returns 0 instead of 0.5
}

Better:

function calculateFee(uint256 amount) public pure returns (uint256) {
    // Minimum fee to prevent zero-fee exploitation
    uint256 fee = amount * 5 / 1000;
    return fee > 0 ? fee : 1;
}

4. Front-running Vulnerabilities

Any transaction that reveals profitable information can be front-run.

Vulnerable (oracle update front-running):

function updatePrice(uint256 newPrice) external onlyOracle {
    price = newPrice; // Attacker sees this in mempool
    // Trades before this transaction confirms
}

Mitigations:

// Commit-reveal scheme
mapping(bytes32 => uint256) public commitments;

function commitPrice(bytes32 hash) external onlyOracle {
    commitments[hash] = block.timestamp;
}

function revealPrice(uint256 price, bytes32 salt) external onlyOracle {
    bytes32 hash = keccak256(abi.encodePacked(price, salt));
    require(commitments[hash] != 0, "No commitment");
    require(block.timestamp > commitments[hash] + 1 hours, "Too early");
    currentPrice = price;
}

5. Denial of Service

Loops with external calls or unbounded iterations are DoS vectors.

Vulnerable:

function distributeRewards() external {
    for (uint i = 0; i < stakers.length; i++) {
        // If one transfer fails, entire distribution fails
        token.transfer(stakers[i], rewards[stakers[i]]);
    }
}

Secure (pull pattern):

function claimReward() external {
    uint256 reward = rewards[msg.sender];
    require(reward > 0, "No reward");
    rewards[msg.sender] = 0;
    token.transfer(msg.sender, reward);
}

Pre-Launch Security Checklist

Before every deployment, we run through this checklist.

Access Control:

  • All admin functions have proper modifiers
  • Owner/admin can't rug (or it's documented)
  • Initialization can only happen once
  • Renounce/transfer ownership works correctly

Value Handling:

  • No reentrancy vulnerabilities
  • External calls follow checks-effects-interactions
  • Proper handling of failed transfers
  • No precision loss in calculations

Logic:

  • Edge cases tested (zero amounts, max values)
  • Loop bounds are limited
  • No DoS vectors
  • Proper event emissions

External Dependencies:

  • Oracle manipulation resistant
  • Flash loan attack resistant
  • Price feeds have freshness checks
  • External contracts verified

Deployment:

  • Constructor parameters verified
  • Proxy initialization correct (if applicable)
  • Initial state is valid
  • Emergency pause functionality works

Testing Strategies That Work

Audits find ~60% of bugs. Good testing finds another 30%.

Unit tests (minimum viable):

describe("Token", function () {
  it("should transfer correctly", async function () {
    await token.transfer(addr1.address, 100);
    expect(await token.balanceOf(addr1.address)).to.equal(100);
  });

  it("should fail on insufficient balance", async function () {
    await expect(
      token.connect(addr1).transfer(addr2.address, 1000)
    ).to.be.revertedWith("Insufficient balance");
  });
});

Fuzzing (where real bugs hide):

// Foundry fuzz test
function testFuzz_Transfer(address to, uint256 amount) public {
    vm.assume(to != address(0));
    vm.assume(amount <= token.balanceOf(address(this)));

    uint256 balanceBefore = token.balanceOf(to);
    token.transfer(to, amount);

    assertEq(token.balanceOf(to), balanceBefore + amount);
}

Invariant testing (catch economic exploits):

function invariant_TotalSupplyConstant() public {
    assertEq(token.totalSupply(), INITIAL_SUPPLY);
}

function invariant_SumOfBalancesEqualsTotalSupply() public {
    uint256 sum = 0;
    for (uint i = 0; i < holders.length; i++) {
        sum += token.balanceOf(holders[i]);
    }
    assertEq(sum, token.totalSupply());
}

DIY Security Tools

Before paying for an audit, run these yourself.

Slither (static analysis):

slither . --filter-paths "node_modules"

Catches: Reentrancy, unused variables, incorrect visibility, many common issues.

Mythril (symbolic execution):

myth analyze contracts/Token.sol

Catches: Integer issues, assertion failures, some reentrancy patterns.

Echidna (fuzzing):

Define invariants, let Echidna try to break them.

Our workflow:

  1. Run Slither on every commit
  2. Run Mythril before testnet deployment
  3. Run Echidna fuzzing for 24+ hours before mainnet
  4. Manual review of all external interactions
  5. Peer review from another developer

Audit Costs and When to Skip

Typical audit costs:

ComplexityLines of CodeCostTimeline
Simple token100-300$5K-10K1-2 weeks
Medium (staking)300-800$10K-25K2-3 weeks
Complex (DeFi)800-2000$25K-50K3-6 weeks
Major protocol2000+$50K-200K4-12 weeks

When to audit:

  • Handling user funds
  • Complex logic (DeFi, bridges)
  • High TVL expected
  • Immutable contracts

When to skip (controversial):

  • Personal projects with no user funds
  • Forkable/upgradeable contracts with no initial TVL
  • Very simple contracts (single function)

Even if skipping formal audit, run automated tools and get peer review.

The Audit Process

What to expect from a professional audit.

Before the audit:

  1. Clean up code (remove unused functions, add comments)
  2. Write comprehensive tests (auditors use your tests)
  3. Document expected behavior
  4. Prepare deployment scripts

During the audit:

  1. Kick-off call to explain the system
  2. Auditors review code (1-4 weeks)
  3. Draft report with findings
  4. You fix issues
  5. Re-review of fixes
  6. Final report

After the audit:

  1. Fix all critical and high issues
  2. Document accepted risks for medium/low
  3. Re-test everything
  4. Deploy with monitoring

Resources

Security Tools:

Learning:

Auditors:

Libraries:

Development:

continue_learning.sh

# Keep building your smart contract knowledge

$ More guides from 100+ contract deployments generating $250M+ volume