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:
- Run Slither on every commit
- Run Mythril before testnet deployment
- Run Echidna fuzzing for 24+ hours before mainnet
- Manual review of all external interactions
- Peer review from another developer
Audit Costs and When to Skip
Typical audit costs:
| Complexity | Lines of Code | Cost | Timeline |
|---|---|---|---|
| Simple token | 100-300 | $5K-10K | 1-2 weeks |
| Medium (staking) | 300-800 | $10K-25K | 2-3 weeks |
| Complex (DeFi) | 800-2000 | $25K-50K | 3-6 weeks |
| Major protocol | 2000+ | $50K-200K | 4-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:
- Clean up code (remove unused functions, add comments)
- Write comprehensive tests (auditors use your tests)
- Document expected behavior
- Prepare deployment scripts
During the audit:
- Kick-off call to explain the system
- Auditors review code (1-4 weeks)
- Draft report with findings
- You fix issues
- Re-review of fixes
- Final report
After the audit:
- Fix all critical and high issues
- Document accepted risks for medium/low
- Re-test everything
- Deploy with monitoring
Resources
Security Tools:
Learning:
- Ethernaut - Security challenges
- Damn Vulnerable DeFi - DeFi exploits
- Smart Contract Programmer - Video tutorials
Auditors:
- Trail of Bits - Top-tier auditor
- OpenZeppelin - Established auditor
- Hacken - Cost-effective audits
- ConsenSys Diligence - Ethereum expertise
Libraries:
- OpenZeppelin Contracts - Audited base contracts
- Solmate - Gas-optimized contracts
Development:
- AllThingsWeb3 Smart Contracts - Development services
- Hardhat - Development environment
- Tenderly - Monitoring and debugging