Smart contracts are the backbone of decentralized applications, automating logic and value exchange on blockchain platforms like Ethereum. However, since these contracts manage real assets and are immutable once deployed, security is paramount. Even a small flaw can lead to significant financial loss or system compromise. Two of the most infamous vulnerabilities that have impacted smart contracts historically are Reentrancy and Integer Overflow/Underflow.
In this guide, we’ll explore these vulnerabilities, their causes, real-world incidents, and mitigation techniques.
1. Reentrancy Attack
What is Reentrancy?
Reentrancy occurs when an external contract is called from within a function before the internal state has been updated, and the external contract then calls back into the original function — potentially repeating the process and draining funds.
How it Works
The vulnerable contract sends Ether to an external contract via .call()
or .transfer()
without updating the state first. The external contract (attacker) has a fallback function that re-calls the vulnerable contract before it finishes execution, exploiting the delay in state change.
Example
// Vulnerable Contract
contract VulnerableBank {
mapping(address => uint) public balances;
function deposit() public payable {
balances[msg.sender] += msg.value;
}
function withdraw() public {
require(balances[msg.sender] > 0);
payable(msg.sender).call{value: balances[msg.sender]}(""); // External call
balances[msg.sender] = 0; // State updated after transfer (too late)
}
}
Attacker Contract
contract Attacker {
VulnerableBank public target;
constructor(address _target) {
target = VulnerableBank(_target);
}
fallback() external payable {
if (address(target).balance > 0) {
target.withdraw(); // Recursive call
}
}
function attack() public payable {
target.deposit{value: msg.value}();
target.withdraw();
}
}
Real-World Incident
The DAO Hack (2016):
Attackers exploited a reentrancy vulnerability to siphon 3.6 million Ether ($60 million at the time), leading to a controversial Ethereum hard fork.
Mitigation Techniques
- Update State Before External Calls
function withdraw() public {
uint amount = balances[msg.sender];
require(amount > 0);
balances[msg.sender] = 0; // Update state first
payable(msg.sender).transfer(amount);
}
- Use
ReentrancyGuard
from OpenZeppelin
contract SecureBank is ReentrancyGuard {
function withdraw() public nonReentrant {
// safe code
}
}
- Avoid Using
call()
for Ether Transfers When Possible: Prefer.transfer()
or.send()
with caution.
2. Integer Overflow and Underflow
What is Integer Overflow?
An overflow occurs when an operation tries to store a number larger than the maximum limit of the data type, causing it to “wrap around” to the beginning of the range.
An underflow is the reverse — when subtraction results in a value below zero in an unsigned integer, wrapping around to the maximum value.
Example
contract Token {
mapping(address => uint256) public balances;
function transfer(address to, uint256 amount) public {
balances[msg.sender] -= amount; // Possible underflow
balances[to] += amount; // Possible overflow
}
}
If msg.sender
has 50 tokens and tries to send 100, the subtraction underflows and sets their balance to a massive number, effectively stealing tokens.
Real-World Incident
BatchOverflow Bug (2018):
A vulnerability in the BatchTransfer function of some ERC-20 tokens allowed attackers to create an enormous token balance via overflow, which was then dumped into exchanges.
Mitigation Techniques
- Use SafeMath Library (for Solidity < 0.8.0)
using SafeMath for uint256;
balances[msg.sender] = balances[msg.sender].sub(amount);
balances[to] = balances[to].add(amount);
- Solidity 0.8.0 and Above
Solidity versions 0.8.0 and later have built-in overflow/underflow protection — it automatically reverts transactions when such operations are detected.
// This will revert automatically on overflow/underflow
balances[msg.sender] -= amount;
balances[to] += amount;
3. Summary Table of Key Differences
Vulnerability | Description | Main Cause | Example Risk | Prevention Methods |
---|---|---|---|---|
Reentrancy | Attacker re-enters the contract before state is updated | External calls before internal updates | DAO Hack (2016) | Update state first, use ReentrancyGuard |
Integer Overflow | Arithmetic exceeds maximum size of the data type | No bounds check on math operations | BatchOverflow Exploit (2018) | Use Solidity >= 0.8 or SafeMath |
Integer Underflow | Arithmetic subtracts below zero for unsigned integers | Lack of input validation | Unauthorized token generation | Input validation, math libraries, newer Solidity |
4. General Best Practices for Secure Smart Contract Development
- Follow the Checks-Effects-Interactions Pattern:
- Always validate conditions, update state, and then interact with external contracts.
- Minimize External Calls:
- Avoid calling untrusted contracts unless absolutely necessary.
- Use Standard Libraries:
- Leverage well-tested libraries like OpenZeppelin for ERC standards, access control, and math.
- Run Automated Tests:
- Use testing frameworks like Hardhat or Truffle to simulate attack vectors.
- Use Static Analysis Tools:
- Tools like Slither, MythX, and Securify help detect vulnerabilities before deployment.
- Audits and Peer Reviews:
- Conduct internal code reviews and third-party audits for all critical contracts.
- Bug Bounties:
- Encourage ethical hackers to identify flaws by offering rewards.