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:
// 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:
// 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
// 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
// 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)
// 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
// 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
// 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
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
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
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
- 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
# Run Slither
slither .
# Run Mythril
myth analyze contract.sol
# Run Custom Security Analysis
npm run security-auditAudit 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
# 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
# Analyze with MythX
myth analyze contract.sol --api-key YOUR_KEY3. Echidna (Fuzzing)
// 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
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
// 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
// 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
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
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:
- 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.