Back to Blog
Real-Life Denial of Service Attacks on Smart Contracts
Web3 SecuritySolidity

Real-Life Denial of Service Attacks on Smart Contracts

10 min
If you have been trying to learn about potential cases of DoS attacks on smart contracts and keep finding the same generic examples, this article is for you. Here we cover real-life findings reported in auditing contests and actual projects, giving you practical insight into how Denial of Service vulnerabilities appear in production code.

What you will learn

  • Real audit findings from competitive auditing platforms
  • Five distinct DoS attack vectors with code examples and recommended fixes
  • How seemingly innocent design choices can freeze entire protocols

DoS caused by underflow

See also: For a broader look at overflow and underflow vulnerabilities — including silent overflow during type casting and real high/medium audit findings — read Overflow & Underflow in Solidity.

The vulnerability: harvestFees() pushes borrowed USDC above borrow cap

In the DnGmxJuniorVaultManager contract, the harvestFees() function grants fees to the senior vault by converting WETH to USDC and staking it directly via Aave.
The problem is that this increases the junior vault's debt indirectly. If the junior vault is already at its borrow cap, the total borrowed amount exceeds the cap, causing availableBorrow to underflow and revert.
Since harvestFees() is called every time a user deposits or withdraws from the junior vault, this effectively locks all deposits and withdrawals.
1if (_seniorVaultWethRewards > state.wethConversionThreshold) {
2 uint256 minUsdcAmount = _getTokenPricsdc(state, state.weth).mulDivDown(
3 _seniorVaultWethRewards * (MAX_BPS - state.slippageThresholdSwapEthBps),
4 MAX_BPS * PRICE_PRECISION
5 );
6 // swaps weth into usdc
7 (uint256 aaveUsdcAmount, ) = state._swapToken(
8 address(state.weth),
9 _seniorVaultWethRewards,
10 minUsdcAmount
11 );
12 // supplies usdc into AAVE
13 state._executeSupply(address(state.usdc), aaveUsdcAmount);
14 // resets senior tranche rewards
15 state.seniorVaultWethRewards = 0;
16}
The borrowed USDC is calculated based on the aUSDC balance:
1function getUsdcBorrowed() public view returns (uint256 usdcAmount) {
2 return uint256(
3 state.aUsdc.balanceOf(address(this)).toInt256() -
4 state.dnUsdcDeposited -
5 state.unhedgedGlpInUsdc.toInt256()
6 );
7}
And the availableBorrow function underflows when borrowed exceeds the cap:
1function availableBorrow(address borrower) public view returns (uint256 availableAUsdc) {
2 uint256 availableBasisCap =
3 borrowCaps[borrower] - IBorrower(borrower).getUsdcBorrowed(); // underflow here
4 uint256 availableBasisBalance = aUsdc.balanceOf(address(this));
5 availableAUsdc = availableBasisCap < availableBasisBalance
6 ? availableBasisCap
7 : availableBasisBalance;
8}

Recommendation

Add a bounds check before the subtraction to prevent underflow:
1function availableBorrow(address borrower) public view returns (uint256 availableAUsdc) {
2 uint256 borrowCap = borrowCaps[borrower];
3 uint256 borrowed = IBorrower(borrower).getUsdcBorrowed();
4
5 if (borrowed > borrowCap) return 0;
6
7 uint256 availableBasisCap = borrowCap - borrowed;
8 uint256 availableBasisBalance = aUsdc.balanceOf(address(this));
9 availableAUsdc = availableBasisCap < availableBasisBalance
10 ? availableBasisCap
11 : availableBasisBalance;
12}

DoS caused by gas limit

The vulnerability: unbounded arrays grow indefinitely

A contract tracks deposits using an ever-growing userDepositsIndex array:
1Receipt[] public deposits;
2mapping(address => uint256[]) public userDepositsIndex;
Every call to depositUSDC pushes to the array:
1function depositUSDC(uint256 _amount) external {
2 require(_amount >= minUSDCAmount, "deposit amount smaller than minimum OTC amount");
3 IERC20(usdc).transferFrom(msg.sender, address(this), _amount);
4
5 usdBalance[msg.sender] = usdBalance[msg.sender] + _amount;
6 deposits.push(Receipt(msg.sender, _amount));
7 userDepositsIndex[msg.sender].push(deposits.length - 1);
8
9 emit USDCQueued(msg.sender, _amount, usdBalance[msg.sender], deposits.length - 1);
10}
The critical issue surfaces when withdrawUSDC iterates over the entire array:
1uint256 toRemove = _amount;
2uint256 lastIndexP1 = userDepositsIndex[msg.sender].length;
3for (uint256 i = lastIndexP1; i > 0; i--) {
4 Receipt storage r = deposits[userDepositsIndex[msg.sender][i - 1]];
5 if (r.amount > toRemove) {
6 r.amount -= toRemove;
7 toRemove = 0;
8 break;
9 } else {
10 toRemove -= r.amount;
11 delete deposits[userDepositsIndex[msg.sender][i - 1]];
12 }
13}
After enough deposits, the gas cost of iterating from lastIndexP1 down to zero exceeds the block gas limit, permanently locking the user's funds.

Recommendation

Remove elements from userDepositsIndex using pop() when deleting deposits, keeping the array bounded and gas costs predictable.

DoS caused by nonReentrant modifier

The vulnerability: internal call chain triggers reentrancy guard on itself

In a staking protocol, multiple functions carry the nonReentrant modifier. The problem arises when unstake() triggers an internal call chain that eventually calls another nonReentrant function:
11. HighStreetPoolBase.unstake() (nonReentrant)
22. └─ HighStreetCorePool._unstake()
33. └─ HighStreetPoolBase._unstake()
44. ├─ HighStreetPoolBase._sync()
55. └─ HighStreetCorePool._processRewards()
66. └─ HighStreetCorePool._processVaultRewards()
77. └─ HighStreetCorePool.transferHighToken() (nonReentrant)
When pendingVaultRewards > 0, the _processVaultRewards function calls transferHighToken:
1function _processVaultRewards(address _staker) private {
2 User storage user = users[_staker];
3 uint256 pendingVaultClaim = pendingVaultRewards(_staker);
4 if (pendingVaultClaim == 0) return;
5
6 uint256 highBalance = IERC20(HIGH).balanceOf(address(this));
7 require(highBalance >= pendingVaultClaim, "contract HIGH balance too low");
8
9 if (poolToken == HIGH) {
10 poolTokenReserve -= pendingVaultClaim > poolTokenReserve
11 ? poolTokenReserve
12 : pendingVaultClaim;
13 }
14 user.subVaultRewards = weightToReward(user.totalWeight, vaultRewardsPerWeight);
15
16 // this call reverts because unstake() already holds the reentrancy lock
17 transferHighToken(_staker, pendingVaultClaim);
18}

Are you audit-ready?

Download the free Pre-Audit Readiness Checklist used by 30+ protocols preparing for their first audit.

No spam. Unsubscribe anytime.

Since unstake() already acquired the reentrancy lock, calling transferHighToken() (which also requires the lock) reverts with ReentrancyGuard: reentrant call. This DoS activates whenever there are pending vault rewards — a normal operational state.

Recommendation

Remove the nonReentrant modifier from transferHighToken() since it is already protected by being called within the guarded unstake() flow.
See also: To understand why nonReentrant exists and how it prevents actual reentrancy attacks, read our deep-dive on Reentrancy Attacks in Solidity — covering the CEI pattern, single-function guards, and the GlobalReentrancyGuard for cross-contract protection.

DoS caused by external call

The vulnerability: blocked Chainlink oracle reverts all dependent functions

When a protocol relies on Chainlink price feeds without a fallback mechanism, a blocked oracle can freeze the entire system:
1function viewPrice(address token, uint collateralFactorBps) external view returns (uint) {
2 if (fixedPrices[token] > 0) return fixedPrices[token];
3 if (feeds[token].feed != IChainlinkFeed(address(0))) {
4 uint price = feeds[token].feed.latestAnswer();
5 require(price > 0, "Invalid feed price");
6 // normalize price ...
7 }
8}
As documented by OpenZeppelin, Chainlink multisigs can block access to price feeds. When latestAnswer() reverts, every function depending on viewPrice() also reverts — creating a cascading DoS across the protocol.

Recommendation

Wrap the oracle call in a try/catch block with fallback logic:
1try feeds[token].feed.latestAnswer() returns (int256 price) {
2 // process price normally
3} catch Error(string memory) {
4 // fallback to alternative price source or cached value
5}

DoS caused by malicious receiver

The vulnerability: attacker-controlled supportsInterface reverts critical functions

In this scenario, anyone can purchase a lien token and have it delivered to an arbitrary receiver contract via buyoutLien:
1function buyoutLien(ILienToken.LienActionBuyout calldata params) external {
2 // ... validation logic ...
3 _transfer(ownerOf(lienId), address(params.receiver), lienId);
4}
A malicious entity can purchase a small lien and route it to a contract that implements supportsInterface() with a deliberate revert. The protocol calls supportsInterface in multiple critical paths, giving the attacker control over those execution flows.
Attack surface 1 — Block endAuction(): Prevents collateral release to auction winners.
1for (uint256 i = 0; i < liensRemaining.length; i++) {
2 ILienToken.Lien memory lien = LIEN_TOKEN.getLien(liensRemaining[i]);
3 if (
4 PublicVault(LIEN_TOKEN.ownerOf(i)).supportsInterface(
5 type(IPublicVault).interfaceId
6 ) // attacker reverts here
7 ) {
8 PublicVault(LIEN_TOKEN.ownerOf(i)).decreaseYIntercept(lien.amount);
9 }
10}
Attack surface 2 — Block liquidate(): Prevents liquidations from starting, allowing unhealthy positions to persist.
Attack surface 3 — Block _payment(): Prevents any lien payments from succeeding when iterating through open liens.

Recommendation

Wrap external supportsInterface calls in a low-level call with a gas limit, or use a try/catch pattern to prevent a single malicious receiver from blocking protocol-wide operations.

Key takeaways

DoS VectorRoot CauseImpact
UnderflowMissing bounds check before subtractionVault deposits/withdrawals frozen
Gas limitUnbounded array iterationUser funds permanently locked
nonReentrantInternal call chain triggers own guardCore staking functions revert
External callNo fallback for oracle failureAll price-dependent functions frozen
Malicious receiverUnchecked external interface callAuctions, liquidations, payments blocked
Going through audit reports is one of the best ways to learn about new exploit patterns. You can use platforms like Solodit to browse findings from competitive auditing platforms and study real vulnerabilities discovered in production protocols.

Get in touch

At Zealynx, we specialize in identifying complex vulnerabilities like Denial of Service attacks that can freeze entire protocols. Whether you are building a DeFi protocol, preparing for an audit, or looking to harden your smart contracts against these attack vectors, our team is ready to assist — reach out.
Want to stay ahead with more in-depth analyses like this? Subscribe to our newsletter and ensure you don't miss out on future insights.

FAQ: Denial of Service attacks on smart contracts

1. What is a Denial of Service attack in the context of smart contracts?
A DoS attack on a smart contract is any exploit that prevents legitimate users from interacting with the contract as intended. Unlike traditional DoS attacks that flood servers with traffic, smart contract DoS attacks exploit logic flaws, gas limitations, or external dependencies to make functions permanently revert or become prohibitively expensive to call.
2. How does an underflow cause a DoS?
In Solidity 0.8+, arithmetic underflow automatically reverts the transaction. If a subtraction like borrowCap - borrowed produces a negative result because borrowed exceeds the cap, the function reverts every time it is called. When this function is in a critical path (like deposits or withdrawals), the entire feature becomes unusable.
3. Why are unbounded loops dangerous in smart contracts?
Every operation in a loop costs gas, and Ethereum blocks have a gas limit. If a loop iterates over an array that grows with each user action, eventually the gas cost of a single transaction exceeds the block gas limit. At that point, the function can never complete, permanently locking any funds or state it controls.
4. Can the nonReentrant modifier itself cause issues?
Yes. The nonReentrant modifier uses a lock that prevents the same contract from being re-entered during execution. If an internal call chain passes through two functions that both have this modifier, the second function will revert because the lock is already held. This is a design bug, not a reentrancy attack.
5. How can protocols protect against oracle-based DoS?
Protocols should wrap oracle calls in try/catch blocks and implement fallback mechanisms such as cached prices, secondary oracles (like Uniswap TWAP), or circuit breakers that pause affected functions gracefully instead of reverting. Never assume an external dependency will always be available.
6. What makes the malicious receiver attack particularly dangerous?
It is dangerous because the attacker only needs to purchase a minimal lien to gain the ability to block protocol-wide operations like auctions, liquidations, and payments. The attack surface is amplified because the protocol checks supportsInterface on the lien owner in multiple critical code paths, allowing a single malicious contract to DoS several independent features simultaneously.

Glossary

TermDefinition
Denial of Service (DoS)An attack that makes a smart contract function unusable by causing it to consistently revert or exceed gas limits.
UnderflowAn arithmetic error where a subtraction produces a negative result, causing an automatic revert in Solidity 0.8+.
Reentrancy GuardA modifier pattern (nonReentrant) that prevents a function from being called again while it is still executing, using a mutex lock.
Gas LimitThe maximum amount of gas a single transaction or block can consume on Ethereum, creating an upper bound on computation per transaction.
OracleAn external data feed (like Chainlink) that provides off-chain information such as asset prices to on-chain smart contracts.

Is Your Protocol Exposed to DoS Vectors?

Gas griefing, unbounded loops, and malicious receiver attacks are among the most overlooked attack surfaces in EVM audits — and the hardest to catch with automated tools alone. If your contracts handle external calls, dynamic arrays, or auction/liquidation mechanics, a targeted review could prevent protocol-wide lockups.

Are you audit-ready?

Download the free Pre-Audit Readiness Checklist used by 30+ protocols preparing for their first audit.

No spam. Unsubscribe anytime.

oog
zealynx

Smart Contract Security Digest

Monthly exploit breakdowns, audit checklists, and DeFi security research — straight to your inbox

© 2026 Zealynx