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
- 1Never Trust User Input: Always validate and sanitize
- 2Least Privilege: Minimize permissions and access
- 3Defense in Depth: Multiple security layers
- 4Fail Securely: Default to secure states
- 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
soliditycontract 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
soliditycontract 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
solidityimport "@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
- 1Code Documentation
- Complete NatSpec comments - Architecture diagrams - Use case documentation
- 1Testing
- Unit tests (>90% coverage) - Integration tests - Edge case tests
- 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
soliditycontract 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
soliditycontract 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
solidityimport "@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:
- 1Defense in Depth: Multiple security layers
- 2Continuous Monitoring: Real-time threat detection
- 3Community Engagement: Bug bounties and responsible disclosure
- 4Professional Audits: Multiple auditors, cross-review
- 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.