You deployed a smart contract on Ethereum. It seems to work. Then someone calls a function multiple times before the first one finishes, and the balance goes to zero. Or an arithmetic overflow lets them mint tokens out of thin air. This is not science fiction: it's reentrancy and overflow, the two black holes of on-chain security.
At Meteora Web, we don't write smart contracts every day, but we work with developers who do, and we've seen projects blow up over a single line of code. We come from accounting: a smart contract bug is like a double-entry balance off by hundreds of thousands of euros. On the blockchain, there's no easy reversal.
This guide is for devs who have already written at least one Solidity contract and want to understand how to prevent reentrancy and overflow, and what to look for in an audit. No abstract theory: real code examples, tools we use, mistakes we've seen.
How does a reentrancy attack work and how to prevent it in a smart contract?
The attack exploits an external call (`call`, `delegatecall`, `send`) made before the contract state is updated. The attacker's contract calls the original function again before the first execution finishes, creating a draining loop.
Sponsored Protocol
Classic vulnerable withdrawal contract
// VULNERABLE
contract VulnerableBank {
mapping(address => uint) public balances;
function withdraw(uint _amount) public {
require(balances[msg.sender] >= _amount);
(bool sent, ) = msg.sender.call{value: _amount}("");
require(sent, "Failed to send");
balances[msg.sender] -= _amount; // updates AFTER call
}
function deposit() public payable {
balances[msg.sender] += msg.value;
}
}
If `msg.sender` is a contract with a `fallback` or `receive` that calls `withdraw` again, funds are drained until the contract's balance is zero.
Prevention: Checks-Effects-Interactions pattern
Simple rule: update state before making external calls.
// SAFE
contract SecureBank {
mapping(address => uint) public balances;
function withdraw(uint _amount) public {
require(balances[msg.sender] >= _amount);
balances[msg.sender] -= _amount; // UPDATE FIRST
(bool sent, ) = msg.sender.call{value: _amount}("");
require(sent, "Failed to send");
}
}
Alternative: use a mutex (lock). OpenZeppelin provides ReentrancyGuard.
Sponsored Protocol
import "@openzeppelin/contracts/security/ReentrancyGuard.sol";
contract SecureBank is ReentrancyGuard {
function withdraw(uint _amount) public nonReentrant {
// logic here
}
}
Tools to detect reentrancy
- Slither: static analysis; detects reentrancy patterns (run
slither . --detect reentrancy). - Mythril: symbolic analysis; finds runtime vulnerabilities.
- Echidna: fuzzing; tests specific properties.
What are arithmetic overflows in smart contracts and how to avoid them?
In Solidity < 0.8, integer overflows don't revert: if a uint8 reaches 256, it wraps to 0. In version >= 0.8, the compiler automatically checks arithmetic overflows (except inside unchecked blocks). But even with 0.8+, some operations in unchecked, derived contracts, or custom libraries can cause overflows.
Example overflow (pre-0.8)
// VULNERABLE (before Solidity 0.8)
contract Token {
mapping(address => uint) public balances;
uint public totalSupply;
function mint(address to, uint amount) public {
totalSupply += amount; // possible overflow
balances[to] += amount;
}
}
If totalSupply is uint256, overflow is unlikely but possible with malicious inputs. In practice, danger comes from implicit casts, operations on uint8, or intermediate calculations.
Sponsored Protocol
Prevention
- Use Solidity >= 0.8 for automatic checks.
- Avoid
uncheckedblocks unless strictly necessary and tested. - Use SafeMath if working with versions < 0.8 (OpenZeppelin SafeMath).
- Watch out for casts:
uint256(uint160(address))is safe, butuint8(uint256)truncates without warning.
What tools should you use for an effective security audit?
An audit is not just tools: it's a process. We recommend a three-layer approach: static, dynamic, and manual.
1. Static analysis with Slither
Slither is our starting point. It analyzes code without executing and detects dozens of vulnerabilities: reentrancy, timestamp dependence, tx.origin, uncontrolled delegatecall, and more.
pip install slither-analyzer
slither . --print human-summary --print contract-summary
2. Dynamic analysis with Foundry
Foundry (forge) lets you write tests in Solidity and run fuzzing. We write property-based tests: "for any input, the total balance remains unchanged".
// Fuzzing test with Foundry
function testWithdrawFuzz(uint amount) public {
vm.assume(amount <= depositBalance);
uint before = address(this).balance;
vault.withdraw(amount);
assert(address(this).balance == before - amount);
}
3. Manual review and economic invariants
No tool catches business logic bugs. Example: a function that lets you withdraw more than expected when combined with a discount. This needs a human auditor. At Meteora Web, when we audit for clients, we write a list of invariants (e.g., "the sum of all balance mappings must equal the contract's Ether balance") and check them one by one.
Sponsored Protocol
Additional tools
- MythX: cloud-based analysis (paid), slower but deeper.
- Echidna: property-based fuzzing, ideal for complex contracts.
- 4nalyzer: reporting tool that combines outputs from multiple scanners.
What other common security bugs appear in smart contracts besides reentrancy and overflow?
Although our focus is reentrancy and overflow, a thorough audit covers at least these:
- Timestamp dependence: using
block.timestampfor randomness -> miner-extractable. - Tx.origin authentication:
tx.origin == ownercan be bypassed via intermediate contract. - Uncontrolled delegatecall: if a contract delegates calls to arbitrary addresses, the caller gains control.
- Front-running: transactions visible in mempool; commit-reveal patterns can mitigate.
How much does an audit cost and how to decide if you need one?
A professional audit for a simple contract (a few hundred lines) starts around $3,000–5,000 USD; complex DeFi protocols can cost $50,000+.
Sponsored Protocol
But the cost of a bug is potentially unlimited: in 2016, a reentrancy in The DAO led to a $60 million theft. Today DeFi projects regularly lose millions to avoidable bugs. Our advice: if your contract handles real value, don't skip the audit. For prototypes, at least run Slither and fuzzing tests.
What to do now
- Review your smart contract and apply the Checks-Effects-Interactions pattern.
- Run
slither . --detect reentrancyand fix every warning. - Check your compiler version: if < 0.8, upgrade or integrate SafeMath.
- Write at least one fuzzing test with Foundry for every state-modifying function.
- If you manage other people's funds, hire an external auditor for a formal review.
Deepen your knowledge with our Blockchain and Web3 for Developers pillar page for a complete overview.
Useful references: Official Solidity Documentation, Slither on GitHub, OpenZeppelin Contracts.