Back to Blog
Smart ContractsSecuritySolidityBlockchain

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

May 4, 2026
11 min read
Smart Contract Security: Best Practices Every Developer Must Know

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 (nonReentrant modifier) 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 public and external function: who should be able to call this?
  • Use OpenZeppelin's Ownable for simple single-owner cases
  • Use AccessControl for 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 management
  • ReentrancyGuard — reentrancy protection
  • Pausable — emergency circuit breaker
  • SafeERC20 — safe token transfer wrappers
  • EIP712 — 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.origin never used for authentication
  • No spot price oracles — Chainlink or TWAP only
  • EIP-712 with nonces on all off-chain signatures
  • Every public/external function 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?
The most common smart contract vulnerabilities are: reentrancy attacks (exploited in the DAO hack), access control failures (missing onlyOwner modifiers), integer overflow/underflow (relevant for pre-Solidity 0.8 contracts), oracle price manipulation, front-running and MEV attacks, and signature replay attacks. Each can lead to complete loss of funds if not properly mitigated.
How much does a smart contract audit cost?
Smart contract audit costs range from $15,000 to $150,000+ depending on code complexity, audit firm reputation, and turnaround time. Code4rena and Sherlock host competitive audit contests that can be more affordable for smaller codebases. The cost is almost always small relative to the TVL the contract will secure.
What is the Checks-Effects-Interactions (CEI) pattern?
The Checks-Effects-Interactions (CEI) pattern is a Solidity coding convention that prevents reentrancy attacks. It requires that every function first validates conditions (checks), then updates state variables (effects), and only then makes external calls to other contracts (interactions). Reversing this order — making external calls before updating state — is the root cause of most reentrancy exploits.
Is Foundry better than Hardhat for smart contract testing?
For most new projects in 2026, Foundry is the preferred testing framework. It is significantly faster than Hardhat, has built-in fuzzing and invariant testing, and allows writing tests in Solidity rather than JavaScript. Hardhat remains relevant for projects with complex JavaScript tooling or existing Hardhat setups, but new projects generally benefit from starting with Foundry.
Do I need a smart contract audit if my contract is small?
If your contract will hold or manage user funds of any amount, an audit is strongly recommended. Many significant exploits have occurred in contracts that appeared simple. At minimum, run automated tools (Slither, Mythril) and have an experienced Solidity developer review the code. For contracts expected to hold more than $100K in value, a professional audit is non-negotiable.
What is a flash loan attack in DeFi?
A flash loan attack uses an uncollateralised DeFi loan (borrowed and repaid within a single transaction) to temporarily manipulate market conditions — such as an oracle price feed or pool balance — in order to exploit a vulnerable contract. The attacker profits from the manipulation and repays the flash loan, all in one atomic transaction. Defences include using TWAP oracles, circuit breakers, and not relying on spot prices for critical decisions.

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