Smart Contract Security: Best Practices Every Developer Must Know
Over $3 billion was lost to smart contract exploits in the last two years. This guide covers the most critical vulnerabilities, real-world exploit patterns, and how to build contracts that hold up in production.
Fahim Zada
Head of Blockchain & Co-Founder

Smart contract security best practices are not optional for any protocol that will hold user funds — they are the difference between a protocol that survives and one that becomes another entry in the DeFi exploit hall of shame. The Ronin Bridge hack ($625M), the Wormhole exploit ($320M), and the Euler Finance attack ($197M) were not caused by incompetent developers. They were caused by specific, identifiable patterns that security-conscious development processes would have caught.
This guide covers the critical vulnerability classes, the testing and tooling workflow that catches them, and the deployment practices that limit damage when something unexpected happens in production.
The Most Critical Smart Contract Vulnerability Classes
Reentrancy
Reentrancy was the vulnerability behind the original DAO hack in 2016, which led to the Ethereum hard fork. It continues to appear in new forms in modern codebases — cross-function reentrancy, cross-contract reentrancy, and read-only reentrancy are all active attack surfaces.
Root cause: A contract makes an external call (to another contract or to a user's address via .call{value: amount}("")) before updating its own state. The recipient's receive() or fallback() function re-enters the original contract and finds state that hasn't been updated yet, allowing it to withdraw funds again.
// VULNERABLE — state update happens AFTER external call
function withdraw(uint amount) external {
require(balances[msg.sender] >= amount);
(bool success,) = msg.sender.call{value: amount}(""); // external call
require(success);
balances[msg.sender] -= amount; // too late — attacker already re-entered
}
// SAFE — Checks-Effects-Interactions pattern
function withdraw(uint amount) external {
require(balances[msg.sender] >= amount); // Check
balances[msg.sender] -= amount; // Effect (state updated FIRST)
(bool success,) = msg.sender.call{value: amount}(""); // Interaction
require(success);
}
Mitigations:
- Always follow the Checks-Effects-Interactions (CEI) pattern — update state before making any external calls
- Use OpenZeppelin's
ReentrancyGuard(nonReentrantmodifier) as a defence-in-depth layer - Be especially vigilant with ERC-777 tokens and flash loan integrations, which create non-obvious external call surfaces
Access Control Failures
Missing or misconfigured access modifiers are responsible for a disproportionate share of DeFi exploits relative to how simple they are to prevent. A function that should be admin-only but is left public or external is an open door.
// VULNERABLE — anyone can call this
function setFeeRecipient(address newRecipient) external {
feeRecipient = newRecipient;
}
// SAFE
function setFeeRecipient(address newRecipient) external onlyOwner {
feeRecipient = newRecipient;
}
Mitigations:
- Audit every
publicandexternalfunction: who should be able to call this? - Use OpenZeppelin's
Ownablefor simple single-owner cases - Use
AccessControlfor role-based permission systems (minter, pauser, admin) - For DAOs and multi-signature governance, integrate a Timelock contract so all admin actions have a mandatory delay before execution — giving users time to exit if they disagree
Integer Arithmetic Issues
Before Solidity 0.8.0, unsigned integer arithmetic silently wrapped around on overflow: type(uint256).max + 1 = 0. Contracts relying on overflow as a feature (before the SafeMath era) still exist in production, and some newer code imports from older libraries that predate 0.8.
Mitigations:
- Use Solidity 0.8+ for all new contracts — overflow/underflow reverts by default
- For any code that deliberately uses
unchecked {}blocks (for gas optimisation), comment exactly why the overflow is safe and add assertions - For legacy contracts still on older Solidity, ensure OpenZeppelin SafeMath is used consistently
Oracle Price Manipulation
Price oracle manipulation has enabled some of the largest DeFi exploits. The attack pattern is consistent: use a flash loan to borrow large amounts, manipulate a spot price used by a vulnerable protocol (e.g., temporarily crash a token price in a single-block AMM), trigger the exploit, and repay the flash loan — all within one transaction.
Mitigations:
- Never use spot prices from a single DEX as a price oracle for any lending, liquidation, or settlement logic
- Use Chainlink price feeds for assets they cover — decentralised, manipulation-resistant, battle-tested
- Use Uniswap V3 TWAPs (Time-Weighted Average Prices) for assets not on Chainlink — the time-weighting makes single-block manipulation economically infeasible
- Add circuit breakers: revert if the reported price deviates more than X% from the previous block's price
- Add maximum deviation checks: compare multiple oracle sources and revert if they diverge significantly
Front-Running and MEV
The Ethereum mempool is public. Validators and MEV searchers monitor every pending transaction and can insert transactions before yours (front-run), after yours (back-run), or sandwich your transaction between two of theirs to extract profit.
For DeFi protocols, MEV-aware design is not optional — if your protocol creates extractable value that you're not capturing, someone else will.
Mitigations:
- Use commit-reveal schemes for any action where the revealed value affects outcome (auctions, NFT mints, random number generation)
- Add slippage protection and minimum output amounts to all DEX interactions to prevent sandwich attacks
- Integrate with Flashbots Protect or MEV Blocker RPCs for transactions from your front-end
- For high-value protocol operations, consider using private mempools or encrypted mempools (EIP-7702 and related proposals)
Signature Replay Attacks
If your contract accepts off-chain signatures to authorise actions, those signatures must be tied to a specific chain, contract address, and nonce. Without these, a valid signature on one chain can be replayed on another, or replayed multiple times on the same chain.
// VULNERABLE — signature contains no chain or nonce binding
bytes32 hash = keccak256(abi.encodePacked(user, amount));
// SAFE — EIP-712 structured data with domain separator
bytes32 hash = _hashTypedDataV4(
keccak256(abi.encode(PERMIT_TYPEHASH, owner, spender, value, nonces[owner]++, deadline))
);
Mitigation: Use EIP-712 for all off-chain signing. It includes a domain separator that binds the signature to a specific contract address and chain ID, and nonces prevent replay within the same context.
Security-First Development Workflow
Start With Battle-Tested Libraries
The goal is to write as little custom security-sensitive code as possible. OpenZeppelin Contracts have been audited thousands of times and battle-tested across billions in TVL:
Ownable/AccessControl— permission managementReentrancyGuard— reentrancy protectionPausable— emergency circuit breakerSafeERC20— safe token transfer wrappersEIP712— structured data hashing for signatures
Do not reimplement these from scratch. Use the OpenZeppelin versions or a well-established fork with equivalent audit history.
Comprehensive Testing With Foundry
Foundry has become the industry standard for Solidity testing because of three capabilities unavailable in Hardhat/Ethers-based testing:
Fuzz testing: Foundry automatically generates thousands of random inputs to your functions and checks that invariants hold. What looks like a simple function often breaks in surprising edge cases.
function testFuzz_depositNeverOverflows(uint256 amount) public {
vm.assume(amount > 0 && amount < type(uint128).max);
vault.deposit(amount);
assertGe(vault.balanceOf(address(this)), 0);
}
Invariant testing: Define properties that must always be true across any sequence of operations, then let Foundry randomly execute sequences to try to violate them.
// This invariant must hold regardless of what sequence of operations occurred
function invariant_totalSupplyNeverExceedsCap() public {
assertLe(token.totalSupply(), token.CAP());
}
Cheatcodes: vm.prank(), vm.warp(), vm.deal() let you simulate specific actors, timestamps, and balances without a running blockchain node.
Target minimum: 100% line coverage, fuzz tests with 100,000+ runs for any financial logic, invariant tests for all core protocol properties.
Static Analysis — Before Every PR
Automated tools run in seconds and catch most common vulnerability patterns before human reviewers spend time on them:
- Slither: The most complete open-source static analyser for Solidity. Run it in CI and treat any high or medium severity finding as a build failure.
- Mythril: Symbolic execution that finds more complex vulnerabilities than pattern matching. Slower — use it pre-audit rather than in every PR.
- Semgrep: Custom rule-based matching. Write rules for project-specific patterns your team wants to enforce.
These tools do not replace audits. They eliminate the noise so that human auditors can focus on the logic and architecture that tools cannot evaluate.
The External Audit — Non-Negotiable for Production
No internal review, no matter how thorough, replaces an independent audit from a firm with no incentive to miss findings. For any contract that will hold meaningful user funds, budget for at least one external audit before mainnet deployment.
Audit options in 2026:
- Trail of Bits — deep security research, complex MEV and economic analysis
- OpenZeppelin — comprehensive Solidity audits, strong ERC standard knowledge
- Spearbit — veteran auditors for high-profile protocols
- Code4rena contests — competitive crowdsourced audits, good value for codebases under $5M TVL target
- Sherlock — audit + insurance model, built-in financial coverage for post-audit exploits
Plan audit timing early: top firms have 4–8 week lead times. An audit is not a one-week checkbox before launch — it's a 2–6 week engagement that often results in significant revisions.
Post-Deployment Defence in Depth
A clean audit does not mean a safe protocol forever. Production brings unexpected user behaviour, token interactions, and market conditions that no audit simulates.
Pausable contracts: The ability to freeze protocol operations in an emergency. Implement pause logic gated by a multisig, with a clear escalation procedure for who can trigger it and under what conditions.
Timelock on admin functions: Any privileged admin action (changing fees, upgrading a contract, changing oracle sources) should go through a timelock of 48–72 hours minimum. This gives users time to withdraw if they disagree with a pending change, and prevents a compromised admin key from instantly draining a protocol.
Bug bounty program: A public bug bounty (Immunefi is the standard platform) incentivises white-hat disclosure before black-hat exploitation. Size the bounty relative to TVL — a $50K bounty protecting $50M in TVL is rational for both parties.
Monitoring and alerting: Use Forta Network agents, OpenZeppelin Defender, or Tenderly's alerting to monitor for abnormal patterns — large withdrawals in short windows, unusual function call patterns, or oracle deviation. An alert at 2am is worth any amount of lost sleep compared to the alternative.
The Legereum Pre-Deployment Security Checklist
Before any production deployment we validate:
- CEI pattern enforced in all state-changing functions
- All external call return values checked
-
tx.originnever used for authentication - No spot price oracles — Chainlink or TWAP only
- EIP-712 with nonces on all off-chain signatures
- Every
public/externalfunction has explicit access control or a documented reason for being permissionless - Slither and Mythril run with zero critical/high findings
- Foundry fuzz tests with minimum 100K runs on financial logic
- Invariant tests covering all core protocol properties
- At minimum one external audit completed and all findings resolved
- Multisig or Timelock on all admin functions
- Emergency pause mechanism implemented and tested
- Upgrade path (if any) documented and secured
Working With Legereum on Smart Contract Security
Security is built into our development process from the architecture stage — not added as a pre-launch review. Our blockchain development team has written and audited production contracts across lending protocols, DEXs, NFT platforms, cross-chain bridges, and real-world asset tokenisation.
If you have an existing contract that needs a security review before launch, or you're building a new protocol and want an experienced team to own the security from day one, get in touch.
Frequently Asked Questions
What are the most common smart contract vulnerabilities?
How much does a smart contract audit cost?
What is the Checks-Effects-Interactions (CEI) pattern?
Is Foundry better than Hardhat for smart contract testing?
Do I need a smart contract audit if my contract is small?
What is a flash loan attack in DeFi?
Building a blockchain product?
Legereum builds and audits smart contracts, DeFi protocols, Web3 apps, and mobile applications. Let's talk about your project.
Get in Touch