Smart contracts automate and secure blockchain transactions without intermediaries, making them essential for Web3 and decentralized apps. However, they are prone to vulnerabilities. In Q1 2024, smart contract exploits led to almost $45 million in losses across 16 incidents, averaging $2.8 million per exploit. This article explores the top 10 most critical smart contract vulnerabilities and how to mitigate them.
Smart contract vulnerabilities can be categorized into several types, each posing significant risks to blockchain applications. Understanding these vulnerabilities is essential for developing robust and secure smart contracts.
Vulnerability | Description |
---|---|
Reentrancy | Exploits the contract’s external call feature, allowing repeated calls before the initial function completes. |
Integer Overflow/Underflow | Results from arithmetic operations that exceed the data type’s storage capacity. |
Improper Access Control | Allow unauthorized users to access or modify contract data or functions (such as unprotected withdrawal) due to inadequate access restrictions |
Front-Running | Exploits the time gap between the transaction’s broadcast and its inclusion in the blockchain. |
Denial of Service (DoS) | Making the contract unavailable or unresponsive by consuming all available gas or causing transactions to continually fail. |
Weak Randomness | Using insecure block-related methods to generate random numbers, which can be manipulated. |
Vulnerable External Calls | Risks associated with making external calls without proper validation. |
Logic Errors | Includes flaws in the contract’s logic leading to unexpected behaviors. |
Oracle Manipulation | Distortion of oracle price feeds or other off-chain data to steal assets. |
Flashloan Attacks | The use of uncollateralized loans to manipulate markets or exploit contract vulnerabilities. |
Smart contract vulnerabilities typically arise from common coding mistakes or logical errors. Issues like unchecked external calls, improper validation, and arithmetic errors can lead to exploits. Let’s delve into specific vulnerabilities and their mitigations.
A reentrancy attack occurs when a contract makes an external call to another contract before updating its state. The called contract can then call back into the original contract, causing unexpected behavior.
There are several types of reentrancy attacks:
contract Deposit {
mapping(address => uint) userBalance;
function deposit() external payable {
userBalance[msg.sender] = msg.value;
}
// this function is vulnerable to reentrancy attacks
function withdraw() external {
require(userBalance[msg.sender] >= 0);
(bool sent,) = payable(msg.sender).call{value: userBalance[msg.sender]}("");
require(sent, "Failed to send Ether");
userBalance[msg.sender] = 0;
}
}
interface IDeposit {
function deposit() external payable;
function withdraw() external;
}
contract AttackDeposit {
IDeposit private depositContract;
constructor(address _target) {
depositContract = IDeposit(_target);
}
function attack() external payable {
require(msg.value == 1 ether, "Invalid attack amount");
depositContract.deposit{value: msg.value}();
depositContract.withdraw();
}
receive() external payable {
if(address(depositContract).balance >= 1 ether) {
depositContract.withdraw();
}
}
}
The vulnerability arises when we send the user their requested amount of ether. In this scenario, the attacker exploits the withdraw()
function. Because their balance hasn’t been reset to 0, they can transfer tokens despite already having received some. The attack involves invoking the withdraw function in the victim’s contract. Upon receiving the tokens, the receive function inadvertently triggers the withdraw function again. As the check passes, the contract sends tokens to the attacker, subsequently activating the receive function.
Rari Capital Hack ($80M)
On April 30, 2022, Rari Capital, a decentralized lending and borrowing platform, was hacked due to a flaw in its borrowed code from Compound. The borrow function lacked proper checks-effects-interactions patterns. The attacker exploited this by:
accountBorrows
mapping. Without a reentrancy guard, the attacker repeatedly called the borrow function before the mapping updated, draining $80 million.
function borrow() external {
…
doTransferOut(borrower, borrowAmount);
// doTransferOut: function doTransferOut(borrower, amount) {
(bool success, ) = to.call.value(amount)("");
require(success, "doTransferOut failed");
}
// !!State updates are made after the transfer
accountBorrows[borrower].principal = vars.accountBorrowsNew;
accountBorrows[borrower].interestIndex = borrowIndex;
totalBorrows = vars.totalBorrowsNew;
…
}
The hacker borrowed assets using a flashloan and ran the doTransferOut
function five times in a loop. After repaying the flashloan, they took the remaining funds and disappeared with $80 million. Transaction: 0xab486012
Orion Protocol ($3M)
On February 2, 2023, the Orion protocol was hacked due to a reentrancy vulnerability in one of its core contracts, resulting in a $3 million loss. The attacker exploited the depositAsset()
method of the ExchangeWithOrionPool contract, which lacked reentrancy protection. They created a fake token (ATK) with a self-destruct feature leading to the transfer()
function.
Use the Checks-Effects-Interactions pattern to ensure state changes occur before external calls.
Vulnerable Implementation
mapping (address => uint) public balances;
function withdraw(uint _amount) public {
require(balances[msg.sender] >= _amount);
(bool success, ) = msg.sender.call{value: _amount}("");
require(success);
balances[msg.sender] -= _amount;
}
Recommended Implementation
function withdraw(uint _amount) public {
require(balances[msg.sender] >= _amount);
balances[msg.sender] -= _amount;
(bool success, ) = msg.sender.call{value: _amount}("");
require(success);
}
In general, to prevent reentrancy attacks, Web3 projects can:
Integer overflow and underflow occur when arithmetic operations exceed the storage capacity of the data type, leading to unexpected results.
Underflow occurs when a value is decreased below zero, while overflow occurs when it exceeds its maximum value. These vulnerabilities can lead to unexpected behavior in smart contracts, potentially resulting in financial losses or system failures.
Consider a uint8 variable, which can hold a maximum of 8 bits. This means the highest number it can store is represented in binary as 11111111 (or in decimal as 2^8 − 1 = 255). In the event of underflow, subtracting 1 from a uint8 set to 0 will result in its value wrapping around to 255. Conversely, overflow occurs when attempting to add 1 to a uint8 set to 255, causing the value to reset back to 0.
contract MyContract {
uint256 public a = type(uint256).min; // Minimum value for uint256, which is 0
uint256 public b = type(uint256).max; // Maximum value for uint256, which is 2^256 - 1
function add() external {
// b == 115792089237316195423570985008687907853269984665640564039457584007913129639935
b = b + 1; // This causes an integer overflow
// After incrementing, b wraps around to 0
// b == 0
}
function substract() external {
// a == 0
a = a - 1; // This causes an integer underflow
// After decrementing, a wraps around to the maximum uint256 value
// a == 115792089237316195423570985008687907853269984665640564039457584007913129639935
}
}
Poolz Finance Hack ($390K)
On March 15th, 2023, Poolz Finance contracts were hacked, resulting in a loss of at least $390K across BSC and Polygon due to an integer overflow vulnerability in the unaudited LockedControl smart contract. The attacker exploited the overflow by manipulating the GetArraySum()
method, which increased the sum beyond its maximum limit, allowing them to withdraw excess tokens into their wallet.
PoWHC Hack ($800K)
Proof of Weak Hands Coin (PoWHC), a Ponzi scheme itself, was exploited due to an integer underflow vulnerability, allowing a hacker to steal 866 ETH. The vulnerability in the “approve” function of the ERC-20 implementation led to the balance of a second account being incorrectly adjusted, resulting in an inflated balance. By manipulating the transferFrom()
and transferTokens()
functions, the attacker caused the second account’s balance to underflow to 2²⁵⁶-1, enabling the theft.
With the release of Solidity 0.8, this concern has been addressed. The compiler now automatically verifies each arithmetic operation for overflow and underflow, and if detected, it throws an error. This alleviates the burden on developers, as they no longer need to manually handle these issues.
For Solidity under version 0.8, use the SafeMath library to prevent overflow and underflow. This library provided functions to safeguard against overflow and underflow vulnerabilities, ensuring the integrity of arithmetic operations in smart contracts.
Vulnerable Implementation
function transfer(address _to, uint256 _value) public {
balances[msg.sender] -= _value;
balances[_to] += _value;
}
Recommended Implementation
using SafeMath for uint256;
function transfer(address _to, uint256 _value) public {
balances[msg.sender] = balances[msg.sender].sub(_value);
balances[_to] = balances[_to].add(_value);
}
An access control vulnerability allows unauthorized users to access or modify a contract’s data or functions. These arise when the code fails to restrict access based on permissions. In smart contracts, access control issues can affect governance and critical functions like minting tokens, voting, withdrawing funds, pausing/upgrading contracts, and changing ownership.
function mint(address account, uint256 amount) public {
// No proper access control is implemented for the mint function
_mint(account, amount);
}
HospoWise Hack
HospoWise was hacked due to a public burn function, allowing anyone to burn tokens. The code’s burn function lacked access control, enabling attackers to burn all Hospo tokens on UniSwap, causing inflation and draining the pool for ETH.
Prevention: Proper access control, such as onlyOwner, or making the function internal.
Rubixy Hack
Rubixy was exploited due to a constructor naming error. The function Fal1out was meant to be the constructor but was callable by anyone, allowing attackers to claim ownership and drain funds.
Prevention: Use proper constructor syntax and careful contract naming.
// This code has not been professionally audited. Use at your own risk.
pragma solidity ^0.8.20;
import "@openzeppelin/contracts/access/AccessControl.sol";
contract MyContract is AccessControl {
bytes32 public constant MANAGER_ROLE = keccak256("MANAGER_ROLE");
bytes32 public constant ADMIN_ROLE = keccak256("ADMIN_ROLE");
address public oracle;
address public treasury;
constructor(address minter) {
_grantRole(DEFAULT_ADMIN_ROLE, msg.sender);
_grantRole(MANAGER_ROLE, minter);
}
function setOracleAddress(address _oracle) external onlyRole(MANAGER_ROLE) {
require(_oracle != address(0), "Invalid oracle address!");
oracle = _oracle;
}
function setTreasuryAddress(address _treasury) external onlyRole(ADMIN_ROLE) {
require(_treasury != address(0), "Invalid treasury address!");
treasury = _treasury;
}
}
In the code above, RBAC assigns user permissions based on predefined roles, providing granular control. It supports multiple roles, like onlyAdminRole and onlyModeratorRole, enhancing security by limiting access to necessary functions. OpenZeppelin’s AccessControl contract simplifies RBAC implementation in smart contracts.
Front-running occurs when a malicious actor preempts a transaction by submitting a similar one with a higher gas fee. Since June 2020, Maximum Extractable Value traders operating bots (aka MEV bots) have profited over $1 billion on Ethereum, BSC, and Solana, often harming retail investors.
Use commit-reveal schemes to obscure bid details until after the bidding period.
Vulnerable Implementation
function placeBid(uint256 _bid) public {
require(_bid > highestBid);
highestBid = _bid;
}
Recommended Implementation
function placeBid(bytes32 _sealedBid) public {
sealedBids[msg.sender] = _sealedBid;
}
function revealBid(uint256 _bid, bytes32 _secret) public {
require(sealedBids[msg.sender] == keccak256(abi.encodePacked(_bid, _secret)));
require(_bid > highestBid);
highestBid = _bid;
}
To protect swapping applications, implement a slippage restriction between 0.1% and 5%, depending on network fees and swap size. This minimizes slippage, defending against front-runners who exploit higher rates, thus safeguarding your trades and reducing predatory risks.
For a detailed guide on front-running attacks and to learn about other mitigation strategies, see Front-Running In Blockchain: Real-Life Examples & Prevention.
DoS attacks can disrupt contract functionality by exploiting reverts, external call failures, and gas limit issues, making it unavailable to legitimate users.
When a contract operation fails, it reverts changes. The EVM uses REVERT (0xFD) and INVALID (0xFE) opcodes to handle these errors, with REVERT returning the remaining gas to the caller and INVALID not returning any gas.
DoS with Unexpected Revert Example
The unexpected revert occurs when the contract attempts to send 1 ether using the call method. If the recipient is a contract that reverts upon receiving Ether, the transaction fails, preventing the beneficiary flag from being reset. This can lead to a DoS condition, blocking further withdrawals if the same address repeatedly attempts to withdraw.
function withdraw() public {
require(beneficiaries[msg.sender]);
beneficiaries[msg.sender] = false;
(bool success, ) = msg.sender.call{value: 1 ether}("");
require(success);
}
DoS with Unexpected Revert Mitigation
Use pull over push payment patterns to prevent DoS. The pull pattern shifts the responsibility of withdrawing funds onto the recipient, preventing the contract from being locked due to failed transfers. A fully mitigated code would store pending withdrawals and allow users to claim them.
function withdraw() public {
require(beneficiaries[msg.sender]);
beneficiaries[msg.sender] = false;
payable(msg.sender).transfer(1 ether);
}
External calls can fail accidentally or deliberately, causing a DoS condition.
External Call Failure Mitigation
Let users withdraw funds rather than push them to them automatically.
Large arrays or loops can exceed the block gas limit, causing a DoS condition.
Gas Limit Vulnerabilities Mitigation
Generating random numbers on Ethereum is challenging due to its deterministic nature. Solidity relies on pseudorandom factors, and complex calculations are costly regarding gas.
Smart contract developers often use insecure block-related methods to generate random numbers, such as the current block timestamp, difficulty, number, address of the current miner, or the hash of a given block. However, these methods can be insecure because miners can manipulate them, affecting the contract’s logic.
function guess(uint256 _guess) public {
uint256 answer = uint256(keccak256(abi.encodePacked(block.timestamp, block.difficulty, msg.sender)));
}
Making external calls without proper validation, like unchecked calls or calls to arbitrary addresses, can lead to unexpected behavior or security risks.
The function doesn’t verify the call’s success or failure. Even if the external call fails, the transaction continues, which can lead to unexpected behavior or vulnerabilities.
function externalCall(address _to) public {
(bool success, ) = _to.call("");
require(success);
}
Checked External Call
The call is validated and the failure is handled.
function externalCall(address _to) public {
require(isValidAddress(_to));
(bool success, ) = _to.call("");
require(success);
}
function isValidAddress(address _addr) internal pure returns (bool) {
return _addr != address(0);
}
Calls to arbitrary addresses refer to interactions with external addresses that are not predetermined or trusted. Attackers can exploit this flaw to run unauthorized code, extract assets, or disrupt the contract’s functionality.
Dexible Exploit ($2M)
On Feb 20, 2023, the Dexible DEX aggregator and execution management system’s self-swapping function was exploited for its external call vulnerability, which allowed users to define a router contract.
contract Dexible {
function selfSwap(address tokenIn, address tokenOut, uint112 uint112 amount, address router, address spender, TokenAmount routeAmount, bytes routerData) external {
IERC20(routeAmount.token).safeApprove(spender, routeAmount.amount);
// Here an external call is made to the router
(bool s, ) = router.call(routerData);
}
}
Instead of a safe&valid DEX, a hacker made a contract to call a malicious ERC20 contract and drained $2M worth of tokens. The crucial part is the contracts were not audited.
contract maliciousRouter {
...
//Instead of a validated router contract, the hacker implements this tricky function and makes Dexible contract to transfer its assets to this malicious contract.
function transfer() external {
IERC20(USDC).transferFrom(msg.sender, address(this), IERC20(USDC).balanceOf(msg.sender));
}
}
.transfer()
and .send()
forward exactly 2,300 gas, which may not be enough for recipients due to changing gas costs. Use .call()
instead and check return values.delegatecall
with untrusted contracts can lead to state changes and potential loss of contract balance.Logic errors in smart contracts can lead to unintended behaviors, compromising security and functionality.
The function blindly adds the provided amount to the sender’s balance without validation, leading to potential issues like overflow and invalid inputs.
function updateBalance(int256 _amount) public {
balances[msg.sender] += _amount;
}
The general idea is to implement thorough testing and code reviews to detect and fix logic errors.
For example, the mitigated code adds a validation check to ensure the amount is positive before updating the balance, preventing overflow and invalid input issues.
function updateBalance(int256 _amount) public {
require(_amount != 0, "Invalid amount");
balances[msg.sender] = balances[msg.sender].add(_amount);
}
Oracles are the blockchain’s gateway to the real world. They connect smart contracts to off-chain data (real-world events, price feeds, random number generation). However, oracle manipulation can significantly distort market prices through methods like spoofing, ramping, bear raids, cross-market manipulation, wash trading, and frontrunning.
Inverse Finance ($15.6M)
Attackers manipulated the price of the INV token using SushiSwap’s TWAP oracle, borrowing $15.6M by depositing inflated INV tokens as collateral. Relying on a single oracle was a major problem.
Lodestar ($6.5M)
A bad actor manipulated the price oracle of plvGLP collateral, enabling them to drain the lending pools and profit approximately $6.5 million. The core vulnerability was in how the GLPOracle calculated the price of plsGLP. The attacker manipulated the price by increasing the total assets via the donate function, which pushed the price higher and allowed the attacker to borrow more than the true value of their collateral.
BonqDAO ($1.8M)
Polygon DeFi protocol BonqDAO fell victim to a price oracle hack due to a smart contract code error. The attacker stole 100 million $BEUR stablecoins and 120 million $WALBT. The exploit was enabled by a vulnerability inside the smart contract for price feed that supplies Bonq protocol with the ALBT price from the Tellor Oracle.
AaveV3 (Prevented)
A vulnerability in the fallback Oracle allowed attackers to set arbitrary asset prices, posing a significant security risk. A third-party audit prevented the possible hack.
Learn more with Blockchain Oracles: Their Importance, Types, And Vulnerabilities.
Flashloan attacks use uncollateralized loans to manipulate markets or exploit contract vulnerabilities within a single transaction block. In Q1 2024, 10 high-profile flashloan attacks resulted in $33M in losses. Flashloans are not inherent vulnerabilities within the contract; rather, attackers use them to increase leverage and magnify the impact of existing smart contract weaknesses.
Beanstalk ($181M)
Beanstalk, a stablecoin protocol with a governance structure, was exploited due to inadequate checks against flashloans in its smart contract. The attacker took a massive flashloan, gained a 78% supermajority, and used the emergencyCommit
function to pass a unanimous proposal, draining funds from the protocol.
function emergencyCommit(uint32 bip) external {
require(isNominated(bip), "Governance: Not nominated.");
// Requires 1 day to be passed (getGovernanceEmergencyPeriod=1day)
require(
block.timestamp >= timestamp(bip).add(C.getGovernanceEmergencyPeriod()), "Governance: Too early.");
require(isActive(bip), "Governance: Ended.");
//Any vote can be executed if proposer has the super majority(getGovernanceEmergencyThreshold=67%)
require(
bipVotePercent (bip). greaterThanOrEqualTo(C.getGovernanceEmergencyThreshold()), "Governance: Must have super majority." );
_execute(msg.sender, bip, false, true);
}
Sonne Finance ($20M)
On May 16, 2024, Sonne Finance was exploited for $20 million due to a known vulnerability in Compound V2 forks. Despite warnings from previous incidents, the protocol failed to implement comprehensive safeguards, allowing an attacker to manipulate governance permissions and drain funds using flash loans.
Learn more about Flashloan Attacks & Prevention.
Security patterns are essential for developing robust smart contracts. Key patterns include:
By adhering to these best practices, developers can build a robust defense system that fortifies their smart contracts’ security:
delegatecall
executes code in the calling contract’s context, preserving msg.sender
and msg.value
.msg.sender
over tx.origin
to enhance security and prevent authorization vulnerabilities.extcodesize
returns zero during contract construction, which can lead to potential issues.Follow @hackenclub on 𝕏 (Twitter)
Given the extensive array of potential vulnerabilities outlined – from specific examples spotlighted to over 30 additional issues identified in our smart contract audit checklist – the threat landscape in the blockchain domain is complex and multifaceted. Smart contract developers are pivotal in creating cutting-edge applications and establishing security measures to protect these contracts from evolving threats. Their challenging task is to stay ahead of these risks, ensuring their applications’ security, integrity, and trustworthiness.
Web3 projects must prioritize security in their blockchain apps. By adhering to guidelines and proactively addressing vulnerabilities, developers can build user trust, protect assets, and contribute to the stability and growth of the blockchain ecosystem. Prioritizing security safeguards individual projects and strengthens the entire decentralized finance space. Let’s work together to create a safer and more secure blockchain environment for everyone.
Be the first to receive our latest company updates, Web3 security insights, and exclusive content curated for the blockchain enthusiasts.
Table of contents
Tell us about your project
10 min read
Discover
6 min read
Discover
11 min read
Discover