Skip to content
All essays
CraftMarch 15, 202518 min

Smart Contract Security: Auditing and Best Practices for 2025

Comprehensive guide to smart contract security. Learn common vulnerabilities, auditing techniques, and best practices to protect your protocols.

Ü
Ümit Uz
Mobile & Full Stack Developer

Smart contract security is the foundation of trustworthy blockchain applications. With billions of dollars at stake, a single vulnerability can be catastrophic. This guide covers essential security practices, common vulnerabilities, and professional auditing techniques for 2025.

The Cost of Security Failures

Before diving into technical details, let's understand the stakes:

  • 2020: $300M+ lost to DeFi hacks
  • 2021: $1.2B+ in crypto exploits
  • 2022: $3.9B+ in hacks and fraud
  • 2023: $1.8B+ in security breaches
  • 2024: $2.1B+ in protocol exploits

Every vulnerability is preventable with proper security practices.

The Security Mindset

Core Principles

  1. 1Never Trust User Input: Always validate and sanitize
  2. 2Least Privilege: Minimize permissions and access
  3. 3Defense in Depth: Multiple security layers
  4. 4Fail Securely: Default to secure states
  5. 5Assume Compromise: Plan for the worst

Common Vulnerabilities

1. Reentrancy Attacks

The most infamous vulnerability, responsible for the DAO hack:

solidity
// VULNERABLE CODE contract VulnerableBank { mapping(address => uint256) public balances; function deposit() public payable { balances[msg.sender] += msg.value; } function withdraw(uint256 amount) public { require(balances[msg.sender] >= amount, "Insufficient balance"); // External call BEFORE state update (bool success, ) = msg.sender.call{value: amount}(""); require(success, "Transfer failed"); // State update AFTER external call balances[msg.sender] -= amount; } }

Attack Vector: Attacker creates a malicious contract that calls withdraw() again in its fallback function.

Fixed Version:

solidity
// SECURE CODE contract SecureBank { mapping(address => uint256) public balances; function deposit() public payable { balances[msg.sender] += msg.value; } function withdraw(uint256 amount) public { require(balances[msg.sender] >= amount, "Insufficient balance"); // State update BEFORE external call balances[msg.sender] -= amount; // Use Checks-Effects-Interactions pattern (bool success, ) = msg.sender.call{value: amount}(""); require(success, "Transfer failed"); } } // Alternative: ReentrancyGuard import "@openzeppelin/contracts/security/ReentrancyGuard.sol"; contract SecureBankWithGuard is ReentrancyGuard { mapping(address => uint256) public balances; function withdraw(uint256 amount) public nonReentrant { require(balances[msg.sender] >= amount, "Insufficient balance"); balances[msg.sender] -= amount; (bool success, ) = msg.sender.call{value: amount}(""); require(success, "Transfer failed"); } }

2. Integer Overflow/Underflow

solidity
// VULNERABLE CODE (Solidity < 0.8.0) contract VulnerableToken { uint256 public totalSupply; mapping(address => uint256) public balances; function transfer(address to, uint256 amount) public { // No overflow check balances[to] += amount; balances[msg.sender] -= amount; } } // SECURE CODE (Solidity >= 0.8.0 has built-in checks) contract SecureToken { uint256 public totalSupply; mapping(address => uint256) public balances; function transfer(address to, uint256 amount) public { require(balances[msg.sender] >= amount, "Insufficient balance"); // Solidity 0.8+ automatically checks for overflow/underflow balances[to] += amount; unchecked { balances[msg.sender] -= amount; } } } // Manual overflow protection for older versions import "@openzeppelin/contracts/utils/math/SafeMath.sol"; contract LegacyToken { using SafeMath for uint256; function transfer(address to, uint256 amount) public { balances[to] = balances[to].add(amount); balances[msg.sender] = balances[msg.sender].sub(amount); } }

3. Access Control Vulnerabilities

solidity
// VULNERABLE CODE contract VulnerableVault { address public owner; uint256 public funds; constructor() { owner = msg.sender; } function withdrawFunds() public { // Anyone can call this! funds = 0; payable(msg.sender).transfer(funds); } } // SECURE CODE import "@openzeppelin/contracts/access/Ownable.sol"; contract SecureVault is Ownable { uint256 public funds; constructor() Ownable(msg.sender) {} function withdrawFunds() public onlyOwner { uint256 amount = funds; funds = 0; payable(owner()).transfer(amount); } // Role-based access control import "@openzeppelin/contracts/access/AccessControl.sol"; bytes32 public constant ADMIN_ROLE = keccak256("ADMIN_ROLE"); bytes32 public constant USER_ROLE = keccak256("USER_ROLE"); constructor() { _grantRole(DEFAULT_ADMIN_ROLE, msg.sender); _grantRole(ADMIN_ROLE, msg.sender); } function adminFunction() public onlyRole(ADMIN_ROLE) { // Only admin can call } }

4. Front-Running (MEV Attacks)

solidity
// VULNERABLE CODE contract VulnerableDEX { mapping(address => uint256) public tokenBalance; uint256 public price; function setPrice(uint256 newPrice) public { price = newPrice; } function buyTokens() public payable { uint256 amount = msg.value / price; tokenBalance[msg.sender] += amount; } } // SECURE CODE - Using Commit-Reveal Pattern contract SecureDEX { struct Commitment { bytes32 commitmentHash; uint256 revealDeadline; bool revealed; address buyer; uint256 amount; } mapping(bytes32 => Commitment) public commitments; // Phase 1: Commit function commitBuy(bytes32 commitmentHash) public { commitments[commitmentHash] = Commitment({ commitmentHash: commitmentHash, revealDeadline: block.timestamp + 1 hours, revealed: false, buyer: address(0), amount: 0 }); } // Phase 2: Reveal function revealBuy( bytes32 commitmentHash, uint256 amount, bytes32 nonce ) public { Commitment storage commitment = commitments[commitmentHash]; require( keccak256(abi.encodePacked(msg.sender, amount, nonce)) == commitmentHash, "Invalid commitment" ); require(block.timestamp < commitment.revealDeadline, "Reveal period expired"); require(!commitment.revealed, "Already revealed"); commitment.revealed = true; commitment.buyer = msg.sender; commitment.amount = amount; // Execute trade tokenBalance[msg.sender] += amount; } }

5. Flash Loan Attacks

solidity
// VULNERABLE CODE contract VulnerableOracle { uint256 public price; function updatePrice(uint256 newPrice) public { price = newPrice; } } contract VulnerableLending { VulnerableOracle public oracle; function borrow(uint256 collateralAmount) public { uint256 price = oracle.price(); // Price manipulation possible here require(collateralAmount * price > 0, "Insufficient collateral"); } } // SECURE CODE - Time-Weighted Average Price (TWAP) import "@uniswap/v3-core/contracts/interfaces/IUniswapV3Pool.sol"; contract SecureOracle { IUniswapV3Pool public pool; function getPrice() public view returns (uint256) { // Use TWAP instead of spot price (uint160 sqrtPriceX96, , , , , , ) = pool.slot0(); return uint256(sqrtPriceX96); } function getTWAP(uint32 period) public view returns (uint256) { uint32[] memory secondsAgos = new uint32[](2); secondsAgos[0] = period; secondsAgos[1] = 0; (int56[] memory tickCumulatives, ) = pool.observe(secondsAgos); uint256 avgTick = uint256(tickCumulatives[1] - tickCumulatives[0]) / period; // Convert tick to price return uint256(int256(1.0001 ** int256(avgTick))); } }

Security Best Practices

1. Use OpenZeppelin Contracts

solidity
// Always use audited, battle-tested libraries import "@openzeppelin/contracts/token/ERC20/ERC20.sol"; import "@openzeppelin/contracts/token/ERC20/extensions/ERC20Burnable.sol"; import "@openzeppelin/contracts/security/ReentrancyGuard.sol"; import "@openzeppelin/contracts/access/Ownable.sol"; import "@openzeppelin/contracts/security/Pausable.sol"; contract MyToken is ERC20, ERC20Burnable, Ownable, ReentrancyGuard, Pausable { constructor() ERC20("MyToken", "MTK") Ownable(msg.sender) { _mint(msg.sender, 1000000 * 10 ** decimals()); } function pause() public onlyOwner { _pause(); } function unpause() public onlyOwner { _unpause(); } function mint(address to, uint256 amount) public onlyOwner whenNotPaused { _mint(to, amount); } }

2. Input Validation

solidity
contract SecureContract { // Always validate addresses modifier validAddress(address addr) { require(addr != address(0), "Invalid address"); require(addr != address(this), "Invalid address"); _; } // Validate amounts modifier validAmount(uint256 amount) { require(amount > 0, "Amount must be greater than 0"); _; } function deposit(address addr, uint256 amount) public payable validAddress(addr) validAmount(amount) { require(msg.value == amount, "Value mismatch"); } // Check for contract addresses when needed function isContract(address addr) public view returns (bool) { uint256 size; assembly { size := extcodesize(addr) } return size > 0; } }

3. Safe External Calls

solidity
contract SafeCalls { // Never use .call() without checks function unsafeCall(address target, uint256 amount) public { (bool success, ) = target.call{value: amount}(""); require(success, "Call failed"); // No gas limit check! } // Safe call pattern function safeCall(address target, uint256 amount) public { (bool success, bytes memory data) = target.call{value: amount, gas: 50000}(""); require(success, "Call failed"); require(data.length == 0 || abi.decode(data, (bool)), "Call reverted"); } // Use OpenZeppelin's Address library using Address for address; function safeTransfer(address token, address to, uint256 amount) public { IERC20(token).safeTransfer(to, amount); } // Batch calls with gas limits function batchCalls( address[] calldata targets, uint256[] calldata values, bytes[] calldata data ) public { require(targets.length == values.length, "Length mismatch"); require(targets.length == data.length, "Length mismatch"); for (uint256 i = 0; i < targets.length; i++) { (bool success, ) = targets[i].call{value: values[i], gas: 100000}(data[i]); require(success, "Batch call failed"); } } }

4. Emergency Stop Mechanism

solidity
import "@openzeppelin/contracts/security/Pausable.sol"; contract EmergencyStoppable is Pausable { uint256 public funds; event EmergencyWithdraw(address indexed admin, uint256 amount); constructor() { _pause(); // Start paused for safety } modifier whenNotPausedOrEmergency() { require(!paused(), "Contract is paused"); _; } function deposit() public payable whenNotPausedOrEmergency { funds += msg.value; } function pause() public onlyOwner { _pause(); } function emergencyWithdraw() public onlyOwner { uint256 amount = address(this).balance; payable(owner()).transfer(amount); emit EmergencyWithdraw(msg.sender, amount); } }

Auditing Checklist

Pre-Audit Preparation

  1. 1Code Documentation

- Complete NatSpec comments - Architecture diagrams - Use case documentation

  1. 1Testing

- Unit tests (>90% coverage) - Integration tests - Edge case tests

  1. 1Static Analysis

- Slither analysis - Mythril analysis - Custom linting rules

Audit Process

bash
# Run Slither slither . # Run Mythril myth analyze contract.sol # Run Custom Security Analysis npm run security-audit

Audit Checklist Items

General Security

  • [ ] No reentrancy vulnerabilities
  • [ ] Proper access control
  • [ ] Input validation on all public functions
  • [ ] Safe external calls
  • [ ] No integer overflow/underflow
  • [ ] Proper error handling
  • [ ] Checked-effects-interactions pattern

Token-Specific

  • [ ] ERC20/ERC721 compliance
  • [ ] Approval race condition protection
  • [ ] Total supply management
  • [ ] Transfer validation

DeFi-Specific

  • [ ] Oracle manipulation protection
  • [ ] Flash loan attack prevention
  • [ ] Liquidation safety
  • [ ] Slippage protection
  • [ ] MEV resistance

Gas Optimization

  • [ ] No unnecessary storage reads
  • [ ] Calldata optimization
  • [ ] Loop optimization
  • [ ] Event emission efficiency

Professional Audit Tools

1. Slither

python
# Slither detector example from slither import Slither from slither.detectors.detectors import reentrancy slither = Slither('contract.sol') results = slither.run_detectors() for result in results: print(f"Vulnerability found: {result}")

2. MythX

bash
# Analyze with MythX myth analyze contract.sol --api-key YOUR_KEY

3. Echidna (Fuzzing)

solidity
// Echidna test example contract TestContract { MyToken token; constructor() { token = new MyToken(); } // Echidna will try to break this invariant function echidna_total_supply_never_decreases() public view returns (bool) { return token.totalSupply() >= token.totalSupply(); } function echidna_no_overflow() public view returns (bool) { return token.totalSupply() <= 115792089237316195423570985008687907853269984665640564039457584007913129639935; } }

Monitoring and Incident Response

On-Chain Monitoring

solidity
contract Monitoring { event LargeTransfer(address indexed from, address indexed to, uint256 amount); event SuspiciousActivity(address indexed user, string details); modifier monitorLargeTransfers(uint256 amount) { _; if (amount > 1000 ether) { emit LargeTransfer(msg.sender, tx.origin, amount); } } function transfer(address to, uint256 amount) public monitorLargeTransfers(amount) { // Transfer logic } }

Off-Chain Monitoring

typescript
// monitoring-service.ts import { ethers } from 'ethers'; class SecurityMonitor { private provider: ethers.Provider; private contract: ethers.Contract; constructor(contractAddress: string, rpcUrl: string) { this.provider = new ethers.JsonRpcProvider(rpcUrl); this.contract = new ethers.Contract(contractAddress, ABI, this.provider); } async monitorLargeTransfers() { this.contract.on('Transfer', (from, to, amount) => { const amountEther = parseFloat(ethers.formatEther(amount)); if (amountEther > 1000) { this.alertSecurityTeam({ type: 'LARGE_TRANSFER', from, to, amount: amountEther, txHash: ethers.getTransactionHash(amount), }); } }); } async monitorReentrancyAttempts() { // Monitor for suspicious patterns const filter = this.contract.filters.Transfer(); const events = await this.contract.queryFilter(filter, -100); // Analyze for reentrancy patterns const addressFrequency = new Map<string, number>(); events.forEach((event: any) => { const addr = event.args.from; addressFrequency.set(addr, (addressFrequency.get(addr) || 0) + 1); }); // Alert on suspicious activity for (const [addr, count] of addressFrequency) { if (count > 10) { this.alertSecurityTeam({ type: 'POTENTIAL_REENTRANCY', address: addr, transactionCount: count, }); } } } private alertSecurityTeam(alert: any) { // Send alert to security team console.log('SECURITY ALERT:', JSON.stringify(alert, null, 2)); // Implement webhook, email, Slack integration } }

Post-Audit Best Practices

1. Bug Bounty Program

typescript
// Bug bounty tiers const BUG_BOUNTY_TIERS = { CRITICAL: { min: 50000, max: 250000 }, HIGH: { min: 10000, max: 50000 }, MEDIUM: { min: 1000, max: 10000 }, LOW: { min: 100, max: 1000 }, }; // Submit vulnerability report async function submitVulnerabilityReport(report: VulnerabilityReport) { const severity = assessSeverity(report); const reward = calculateReward(severity); // Verify vulnerability const valid = await verifyVulnerability(report); if (valid) { await processReward(report.researcher, reward); await notifyDevelopmentTeam(report); } }

2. Gradual Rollout

solidity
contract GradualRollout { uint256 public maxSupply = 1000000 * 10**18; uint256 public currentSupply = 0; uint256 public rolloutPhase = 1; modifier rolloutLimit(uint256 amount) { uint256 limit = maxSupply * rolloutPhase / 10; // 10% per phase require(currentSupply + amount <= limit, "Rollout limit exceeded"); _; } function mint(address to, uint256 amount) public rolloutLimit(amount) { currentSupply += amount; _mint(to, amount); } function advancePhase() public onlyOwner { require(rolloutPhase < 10, "Fully rolled out"); rolloutPhase++; } }

3. Time-Lock Implementation

solidity
import "@openzeppelin/contracts/governance/TimelockController.sol"; contract TimeLockedGovernance { TimelockController public timelock; uint256 public constant MIN_DELAY = 2 days; constructor() { timelock = new TimelockController( MIN_DELAY, new address[](0), // No proposers initially new address[](0), // No executors initially msg.sender ); } function proposeAction(bytes calldata data) public { // Actions must go through timelock timelock.schedule(address(this), 0, data, 0, bytes32(0), bytes32(0), MIN_DELAY); } }

Conclusion

Smart contract security is an ongoing process, not a one-time event. The 2025 landscape requires:

  1. 1Defense in Depth: Multiple security layers
  2. 2Continuous Monitoring: Real-time threat detection
  3. 3Community Engagement: Bug bounties and responsible disclosure
  4. 4Professional Audits: Multiple auditors, cross-review
  5. 5Gradual Deployment: Phased rollout with monitoring

Remember: In blockchain, code is law, and bugs can cost millions. Invest in security from day one.

The most secure contracts are those that assume vulnerabilities exist and plan accordingly. Stay vigilant, test thoroughly, and never compromise on security.

Next essay
Rust to WebAssembly: Building WASM Modules