Smart contract development on the Ethereum blockchain has evolved significantly over the past few years, yet security remains a top concern for developers and organizations alike. In this comprehensive analysis, we revisit and update the most prevalent security vulnerabilities found in Solidity-based smart contracts. Originally studied in 2018, these issues have shifted in frequency and severity—reflecting improvements in developer awareness, compiler upgrades, and best practices.
By understanding the core risks in modern contract design—such as unchecked external calls, arithmetic precision errors, and owner privilege abuse—you can write safer, more resilient code. This guide identifies the top 10 common Solidity security issues observed in 2025, offering actionable insights and secure coding patterns.
Key Observations Since 2018
Before diving into the updated list, it’s important to highlight some critical trends that didn’t make the top 10 but still pose significant risks:
- Reentrancy and external denial-of-service, once dominant threats, have declined due to better tooling and defensive programming—though they remain dangerous, especially with DeFi composability (e.g., flash loans).
- Over 50% of scanned contracts still do not support Solidity v0.5.0+, let alone v0.6.x or later, which introduced major security enhancements.
- Around 30% use deprecated syntax like
sha3,throw, orconstant, indicating outdated development practices. - A staggering 83% have improper pragma version specifications, leaving them vulnerable to compiler-related exploits.
- Visibility misconfigurations (e.g., public functions mistakenly exposed) increased by 48%, even though they didn’t rank in the top 10.
👉 Discover how secure coding practices can prevent costly exploits before deployment.
1. Unchecked External Calls
Ranked #1 in 2025, unchecked external calls are now the most widespread Solidity vulnerability.
Unlike high-level function calls (contract.doSomething()), low-level methods like address.call() do not automatically revert on failure—they return a boolean false. If this return value isn’t checked, the transaction continues as if nothing went wrong.
Example of Risk:
addr.call{value: 1 ether}("");
// No error thrown if call failsSecure Pattern:
Always check the result:
(bool success, ) = addr.call{value: 1 ether}("");
require(success, "Transfer failed");Even send() should be validated:
if (!addr.send(1)) {
revert("Payment failed");
}This issue overlaps with “unsafe transfers” but extends to any external interaction, including delegatecalls and library invocations.
2. High-Cost Loops
Now second on the list, high-cost loops affect nearly 8% more contracts than in previous years.
Ethereum gas costs scale with computation. Loops that iterate over user-expandable arrays can become prohibitively expensive—or worse, lead to denial-of-service (DoS) if an attacker inflates the array size.
Vulnerable Code:
for (uint i = 0; i < users.length; i++) {
users[i].claimReward();
}If users.length grows too large, the loop may exceed block gas limits, preventing execution.
Best Practice:
Use pull-over-push patterns or off-chain solutions (e.g., Merkle trees) to minimize on-chain iteration.
3. Overprivileged Owner
A new entry in the top 10, this issue affects ~16% of contracts.
Many contracts rely on an owner role with exclusive access to critical functions. While access control is necessary, excessive privileges create single points of failure.
Risk Scenario:
modifier onlyOwner() {
require(msg.sender == owner);
_;
}
function emergencyWithdraw() public onlyOwner {
payable(owner).transfer(address(this).balance);
}If the owner’s private key is compromised, attackers gain full control.
👉 Learn how multi-signature wallets and decentralized governance reduce single-point risks.
4. Arithmetic Precision Errors
Due to the EVM’s lack of native floating-point support, integer division can cause significant rounding errors.
Problematic Code:
function calculateBonus(uint amount) public pure returns (uint) {
return (amount / 100) * 5; // May truncate prematurely
}If amount is less than 100, the result becomes zero.
Solution:
Reorder operations or use fixed-point math libraries:
return (amount * 5) / 100; // Safer orderConsider using OpenZeppelin’s SafeMath or Solidity’s built-in overflow checks (v0.8+).
5. Dependency on tx.origin
Using tx.origin for authorization opens the door to phishing and relay attacks.
Why It's Dangerous:
tx.originis the original external account that initiated the call chain.msg.senderis the immediate caller (can be a contract).
An attacker can deploy a malicious contract that calls your contract, passing tx.origin == owner check—even if msg.sender is not trusted.
Insecure:
require(tx.origin == owner);Secure:
require(msg.sender == owner);Always prefer msg.sender for access control.
6. Integer Overflow and Underflow
Though mitigated in Solidity v0.8+ (which includes built-in checks), many legacy contracts remain vulnerable.
Underflow Example:
for (uint i = 10; i >= 0; i--) { ... }When i == 0, decrementing wraps to 2^256 - 1, creating an infinite loop.
Use safe counters:
while (i > 0) {
i--;
// logic
}Or upgrade to Solidity v0.8+ where overflows revert by default.
7. Unsafe Type Inference
In older versions of Solidity (<0.6), var allowed type inference, leading to unexpected behavior.
Example:
for (var i = 0; i < elements.length; i++) { }Here, i may be inferred as uint8. If elements.length > 255, overflow occurs.
Note: var was removed in Solidity v0.6. Always declare types explicitly:for (uint256 i = 0; i < elements.length; i++) { }8. Insecure Ether Transfers
This issue dropped from #6 to #8 but still exists in legacy codebases.
While transfer() and send() both forward ether, only transfer() reverts automatically on failure.
Preferred:
payable(addr).transfer(amount); // Reverts if failsAvoid:
bool success = addr.send(amount);
if (!success) revert();Even better: use call with proper checks in v0.8+.
9. Ether Transfers Inside Loops
Sending ether within a loop risks partial execution or DoS.
Vulnerability:
for (uint i = 0; i < users.length; i++) {
users[i].transfer(amount); // Fails if one user rejects ETH
}One non-payable contract halts the entire process.
Fix:
Implement a withdrawal pattern instead:
function claim() public {
uint amount = pendingPayments[msg.sender];
pendingPayments[msg.sender] = 0;
payable(msg.sender).transfer(amount);
}10. Timestamp Dependence
Block timestamps (block.timestamp) are miner-controlled and thus unreliable for critical logic.
Risky Use:
if (block.timestamp >= endTime) {
endAuction();
}Miners can manipulate timestamps within limits to front-run auctions or influence outcomes.
Best Practice:
Use block numbers for time-based logic when possible, or leverage decentralized oracles like Chainlink VRF for randomness and timing.
Frequently Asked Questions (FAQ)
Q: Can I ignore these issues if I'm using the latest Solidity version?
A: No. While newer versions (v0.8+) include built-in overflow protection and remove unsafe features like var, many vulnerabilities—such as unchecked calls and owner privilege abuse—are logic-level flaws unaffected by compiler updates.
Q: How can I test my contract for these issues?
A: Use automated tools like Slither, MythX, or Hardhat’s security plugins. Combine static analysis with manual audits and formal verification for high-stakes projects.
Q: Is reentrancy still a threat?
A: Yes. Despite its absence from this year’s top 10, reentrancy remains dangerous—especially in DeFi protocols using flash loans. Always apply the Checks-Effects-Interactions pattern.
Q: What’s the best way to manage contract ownership?
A: Avoid centralized control when possible. Use multi-sig wallets, timelocks, or decentralized governance models to reduce risk.
Q: Why are so many contracts still using outdated Solidity versions?
A: Legacy codebases, dependency constraints, and lack of maintenance contribute to slow adoption. However, upgrading improves security and enables modern language features.
Q: How often should I audit my smart contracts?
A: Audit before deployment and after any major update. For active protocols, consider quarterly reviews or continuous monitoring with on-chain detection tools.
Solidity development demands rigor. With immutable code and high-value assets at stake, even minor oversights can lead to irreversible losses. By addressing these top 10 security pitfalls—from unchecked calls to timestamp dependence—you significantly improve your contract’s resilience.
Adopt secure coding standards, leverage modern tooling, and prioritize audits. The future of decentralized applications depends on trust—and trust begins with code you can verify.