Denial of service in a smart contract is not a network-layer problem. There is no flood of packets to filter. The attack surface lives entirely inside the EVM — inside loops that grow without bound, inside payment pipelines that hand control to untrusted receivers, inside governance thresholds that a patient attacker can suppress. In Solidity, DoS is a vulnerability class that disrupts the expected execution of contract functions by exhausting resources or blocking the contract’s operation. In the blockchain world, code represents the flow of funds or the execution of internal logic — and in severe cases, DoS can directly result in asset immobilization and losses for users or protocols.

This article catalogues every major DoS pattern. For each one: vulnerable code, the attack scenario, and the correct design.


1. Unbounded Loops and Gas Griefing

Why It Happens

A particularly widespread class of security vulnerabilities that afflicts Ethereum smart contracts is the gas limit denial of service via unbounded operations. These vulnerabilities result in a failed transaction with an out-of-gas error and are often present in contracts containing loops whose bounds are affected by end-user input.

The key idea is that there exists some execution of a smart contract that would cause it to expend all its transaction gas, forcing an out-of-gas exception and causing a denial of service. Every operation in an Ethereum smart contract costs a certain amount of gas — a measurement unit for the computational effort required to execute that operation on the EVM, paid by the transaction initiator.

Vulnerable Code

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

contract VulnerableDistributor {
    address[] public participants;
    uint256 public rewardPool;

    function addParticipant(address user) external {
        participants.push(user);
    }

    // ❌ Unbounded loop: grows indefinitely with each participant added
    function distributeRewards() external {
        uint256 share = rewardPool / participants.length;
        for (uint256 i = 0; i < participants.length; i++) {
            (bool ok, ) = participants[i].call{value: share}("");
            require(ok, "Transfer failed");
        }
        rewardPool = 0;
    }

    receive() external payable {
        rewardPool += msg.value;
    }
}

Attack Scenario

A bad actor creates a significant number of addresses, each being paid a small amount from the contract. If done effectively, the transaction can be blocked indefinitely, possibly preventing further transactions from going through. If the array lives in storage and grows without a cap, the gas cost of iterating over it grows linearly with array size. Eventually, a transaction that calls the function exceeds the block gas limit and reverts — permanently. This makes the function uncallable without a contract upgrade, constituting a denial-of-service condition on that function.

Gas griefing is a related attack where a caller sends exactly enough gas to reach a subcall but not enough for the subcall to complete. Gas griefing occurs when a user sends the amount of gas required to execute the target smart contract but not enough to execute subcalls — calls it makes to other contracts.

Correct Design

Use a paginated (chunked) approach so no single transaction processes the full array. Cap batch size explicitly and track position in state.

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

contract SafeDistributor {
    address[] public participants;
    uint256 public rewardPool;
    uint256 public nextIndex;
    uint256 public constant BATCH_SIZE = 50;

    function addParticipant(address user) external {
        participants.push(user);
    }

    // ✅ Paginated distribution: processes BATCH_SIZE participants per call
    function distributeRewardsBatch() external {
        uint256 end = nextIndex + BATCH_SIZE;
        if (end > participants.length) end = participants.length;

        uint256 share = rewardPool / participants.length;

        for (uint256 i = nextIndex; i < end; i++) {
            (bool ok, ) = participants[i].call{value: share}("");
            // Continue on failure — do not let one bad receiver block the batch
            if (!ok) emit TransferFailed(participants[i], share);
        }

        nextIndex = end;
        if (nextIndex == participants.length) {
            nextIndex = 0;
            rewardPool = 0;
        }
    }

    event TransferFailed(address indexed recipient, uint256 amount);

    receive() external payable {
        rewardPool += msg.value;
    }
}

A good solution is to implement partial distribution logic so that rewards can be distributed in smaller batches. Add an explicit maximum iteration cap, or implement pagination so the caller passes a start index and batch size. Consider replacing unbounded arrays with EnumerableSet from OpenZeppelin, which supports safe iteration patterns.


2. Push vs. Pull Payment Patterns

Why It Happens

Some smart contracts implement a “push” payment model where the contract directly sends funds to recipients. While this approach seems straightforward, it introduces a critical vulnerability: the contract’s ability to function becomes dependent on the recipient’s ability to receive funds.

The receiving address can implement a fallback function that can throw an error. Thus, we should never trust that a send call will execute without error.

Vulnerable Code

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

contract VulnerableAuction {
    address public highestBidder;
    uint256 public highestBid;
    mapping(address => uint256) public pendingRefunds;

    function bid() external payable {
        require(msg.value > highestBid, "Bid too low");

        address prevBidder = highestBidder;
        uint256 prevBid = highestBid;

        highestBidder = msg.sender;
        highestBid = msg.value;

        // ❌ Push pattern: refund is sent immediately to the previous bidder
        if (prevBidder != address(0)) {
            (bool ok, ) = prevBidder.call{value: prevBid}("");
            require(ok, "Refund failed"); // If this reverts, no one can bid
        }
    }
}

Attack Scenario

Some contracts process all withdrawals in a single transaction. If one person’s withdrawal fails, the entire transaction reverts, blocking everyone else. Attackers exploit this by deliberately making their withdrawal fail, locking up funds for everyone.

A malicious bidder deploys a contract with a receive() that always reverts:

contract MaliciousBidder {
    VulnerableAuction public auction;

    constructor(address _auction) {
        auction = VulnerableAuction(_auction);
    }

    function attack() external payable {
        auction.bid{value: msg.value}();
    }

    // ❌ Always reverts incoming ETH — permanently blocks refund push
    receive() external payable {
        revert("I reject your ETH");
    }
}

When a legitimate user later tries to outbid MaliciousBidder, the contract attempts to push a refund to the attacker’s contract, which reverts, which causes the entire bid() call to revert. No future bids are possible.

Correct Design

It is often better to isolate each external call into its own transaction that can be initiated by the recipient of the call. This is especially relevant for payments, where it is better to let users withdraw funds rather than push funds to them automatically. This also reduces the chance of problems with the gas limit.

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

contract SafeAuction {
    address public highestBidder;
    uint256 public highestBid;
    mapping(address => uint256) public pendingRefunds;

    function bid() external payable {
        require(msg.value > highestBid, "Bid too low");

        // ✅ Credit the previous bidder's balance — do NOT push ETH
        if (highestBidder != address(0)) {
            pendingRefunds[highestBidder] += highestBid;
        }

        highestBidder = msg.sender;
        highestBid = msg.value;
    }

    // ✅ Pull: each user initiates their own withdrawal
    function withdraw() external {
        uint256 amount = pendingRefunds[msg.sender];
        require(amount > 0, "Nothing to withdraw");

        pendingRefunds[msg.sender] = 0; // Zero before send (reentrancy guard)
        (bool ok, ) = msg.sender.call{value: amount}("");
        require(ok, "Withdrawal failed");
    }
}

The pull over push pattern is a good way to mitigate some of the quirks that come with Solidity when sending ether, especially when performing multiple transfers at once. Due to the isolation of the error-prone transfer functionality, one failed transfer does not lead to a revert of all successful operations anymore. Additionally, it is now the responsibility of the requesting user to make sure that they are able to receive ether.


3. Reverting Receivers That Block Protocol Flow

Why It Happens

This pattern is a superset of the push problem but manifests in non-auction contexts — anywhere a protocol makes an unconditional external call in a critical code path. The receiver does not even need to be malicious; implementing complex logic — such as a gas-consuming loop — can inadvertently lead to a revert.

Vulnerable Code

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

interface IFeeRecipient {
    function onFeeReceived(uint256 amount) external;
}

contract VulnerableProtocol {
    address public feeRecipient;
    uint256 public accumulatedFees;

    constructor(address _feeRecipient) {
        feeRecipient = _feeRecipient;
    }

    // ❌ Every swap calls an external hook that can revert, blocking ALL swaps
    function swap(uint256 amountIn) external returns (uint256 amountOut) {
        amountOut = _calculateOut(amountIn);
        uint256 fee = amountIn / 100;
        accumulatedFees += fee;

        // If feeRecipient reverts, swap is permanently broken
        IFeeRecipient(feeRecipient).onFeeReceived(fee);

        return amountOut;
    }

    function _calculateOut(uint256 amountIn) internal pure returns (uint256) {
        return (amountIn * 997) / 1000;
    }
}

Attack Scenario

The feeRecipient is set to a contract that either intentionally reverts in onFeeReceived, or whose owner pauses it. Every call to swap() reverts. The protocol is dead until feeRecipient is changed — if the setter even exists.

Correct Design

Use try/catch to isolate external calls that are not strictly required for the core invariant:

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

interface IFeeRecipient {
    function onFeeReceived(uint256 amount) external;
}

contract SafeProtocol {
    address public feeRecipient;
    uint256 public accumulatedFees;
    uint256 public undeliveredFees;

    event FeeDeliveryFailed(address recipient, uint256 amount);

    constructor(address _feeRecipient) {
        feeRecipient = _feeRecipient;
    }

    // ✅ External hook failure is caught; swap proceeds regardless
    function swap(uint256 amountIn) external returns (uint256 amountOut) {
        amountOut = _calculateOut(amountIn);
        uint256 fee = amountIn / 100;
        accumulatedFees += fee;

        try IFeeRecipient(feeRecipient).onFeeReceived(fee) {
            // Success path — no action needed
        } catch {
            // Graceful degradation: accumulate fees for later rescue
            undeliveredFees += fee;
            emit FeeDeliveryFailed(feeRecipient, fee);
        }

        return amountOut;
    }

    function _calculateOut(uint256 amountIn) internal pure returns (uint256) {
        return (amountIn * 997) / 1000;
    }
}

To prevent denial of service scenarios, it is recommended to query external dependencies using a defensive approach with Solidity’s try/catch structure. In this way, if the call fails, the caller contract is still in control and can handle any errors safely and explicitly.


4. Block Gas Limit DoS

Why It Happens

In Ethereum, every operation performed by a smart contract consumes a certain amount of gas. The block gas limit is the maximum amount of gas that can be used in a single block. If a function requires more gas than the block gas limit to complete, the transaction will fail. This vulnerability is particularly common in loops that iterate over dynamic data structures where the number of iterations is not fixed and can grow arbitrarily large.

Block gas limit DoS has an additional active attack variant — block stuffing. A block stuffing attack was used on a gambling DApp, Fomo3D. The app had a countdown timer, and users could win a jackpot by being the last to purchase a key — every time a user bought a key, the timer was extended. An attacker bought a key then stuffed the next 13 blocks in a row to win the jackpot.

Vulnerable Code

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

contract VulnerableLottery {
    address[] public players;
    uint256 public ticketPrice = 0.01 ether;

    function enter() external payable {
        require(msg.value == ticketPrice, "Wrong ticket price");
        players.push(msg.sender);
    }

    // ❌ As players grows into the thousands, this exceeds the block gas limit
    function pickWinner() external {
        uint256 winnerIndex = uint256(
            keccak256(abi.encodePacked(block.timestamp, block.prevrandao))
        ) % players.length;

        address winner = players[winnerIndex];

        // Iterates over ALL players to reset — gas grows linearly
        delete players;

        (bool ok, ) = winner.call{value: address(this).balance}("");
        require(ok, "Prize transfer failed");
    }
}

Attack Scenario

As players.length grows, delete players iterates over every slot to zero it. At sufficient scale the function permanently exceeds the block gas limit. Having too large an array of users to send funds to can exceed the gas limit and prevent the transaction from ever succeeding, potentially permanently locking up funds.

Correct Design

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

contract SafeLottery {
    address[] public players;
    uint256 public ticketPrice = 0.01 ether;
    uint256 public roundId;

    event WinnerPicked(uint256 indexed roundId, address winner, uint256 prize);

    function enter() external payable {
        require(msg.value == ticketPrice, "Wrong ticket price");
        players.push(msg.sender);
    }

    // ✅ O(1) reset: swap last element into winner slot, then pop
    function pickWinner() external {
        require(players.length > 0, "No players");

        uint256 winnerIndex = uint256(
            keccak256(abi.encodePacked(block.timestamp, block.prevrandao, roundId))
        ) % players.length;

        address winner = players[winnerIndex];
        uint256 prize = address(this).balance;

        // O(1) array wipe — replaces reset loop
        players[winnerIndex] = players[players.length - 1];
        players.pop();

        // If the round should truly reset, start fresh mapping instead
        roundId++;
        emit WinnerPicked(roundId, winner, prize);

        (bool ok, ) = winner.call{value: prize}("");
        require(ok, "Prize transfer failed");
    }
}

The key technique is the swap-and-pop pattern: instead of zeroing the entire array (O(n)), move the last element into the target slot and pop() (O(1)). For cases requiring full resets, use a roundId mapping so old entries are simply never read.


5. Storage Layout DoS (Reading Many Storage Slots)

Why It Happens

Solidity allocates storage slots sequentially, starting from slot 0. Each storage slot is 32 bytes (256 bits), and variables are packed when possible to optimize gas costs. This deterministic layout forms the foundation of how contracts persist data on the Ethereum blockchain. The EVM uses a key-value store where each contract has 2²⁵⁶ storage slots, accessed through SLOAD and SSTORE operations.

Cold SLOAD (EIP-2929, since Berlin) costs 2,100 gas per slot. A function that reads N distinct storage slots in a loop pays 2,100 × N gas from cold state. An attacker that forces that loop to iterate over thousands of slots can push the cost past the block gas limit.

Vulnerable Code

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

contract VulnerableVault {
    struct Position {
        address owner;     // slot word 0
        uint128 amount;    // slot word 1a
        uint128 timestamp; // slot word 1b
        bool    active;    // slot word 2
        bytes32 metadata;  // slot word 3
    }

    // Each position occupies multiple storage slots
    mapping(uint256 => Position) public positions;
    uint256[] public activePositionIds;

    function openPosition(uint128 amount, bytes32 meta) external {
        uint256 id = activePositionIds.length;
        positions[id] = Position(msg.sender, amount, uint128(block.timestamp), true, meta);
        activePositionIds.push(id);
    }

    // ❌ Reads every storage slot for every active position — O(n * slots_per_position)
    function totalActiveValue() external view returns (uint256 total) {
        for (uint256 i = 0; i < activePositionIds.length; i++) {
            Position storage p = positions[activePositionIds[i]];
            if (p.active) {
                total += p.amount; // cold SLOAD per position
            }
        }
    }
}

Attack Scenario

A griever opens thousands of small positions. totalActiveValue() now triggers thousands of cold SLOADs, exceeding the block gas limit. Any protocol function that gates on this view (e.g., requires knowing total value before allowing withdrawals) is permanently broken.

Correct Design

Maintain a running aggregate in a single storage slot. Update it on every write. Reading the aggregate is always O(1) regardless of position count.

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

contract SafeVault {
    struct Position {
        address owner;
        uint128 amount;
        uint128 timestamp;
        bool    active;
        bytes32 metadata;
    }

    mapping(uint256 => Position) public positions;
    uint256 public nextPositionId;

    // ✅ Running aggregate — O(1) read regardless of how many positions exist
    uint256 public totalActiveValue;

    event PositionOpened(uint256 indexed id, address owner, uint128 amount);
    event PositionClosed(uint256 indexed id);

    function openPosition(uint128 amount, bytes32 meta) external {
        uint256 id = nextPositionId++;
        positions[id] = Position(msg.sender, amount, uint128(block.timestamp), true, meta);

        // Update aggregate on write — one SSTORE, not N SLOADs on read
        totalActiveValue += amount;

        emit PositionOpened(id, msg.sender, amount);
    }

    function closePosition(uint256 id) external {
        Position storage p = positions[id];
        require(p.owner == msg.sender, "Not owner");
        require(p.active, "Already closed");

        p.active = false;
        totalActiveValue -= p.amount;

        emit PositionClosed(id);
    }
}

Storage layout review during audits examines whether variables are efficiently packed and correctly ordered. Auditors analyze: variable types and sizes, declaration order, packing opportunities, and storage access patterns. Prefer derived state (aggregates) maintained incrementally over recomputed state requiring full traversal.


6. Integration DoS (Third-Party Dependency That Can Be Made to Fail)

Why It Happens

Serious oracle failures can put billions of dollars deposited in DeFi contracts at risk. The ever-increasing number of DeFi projects almost always rely on a small set of price oracles. Failure in any one of these price oracles could lead to a devastating domino effect felt across the entire ecosystem.

While currently there is no whitelisting mechanism to allow or disallow contracts from reading prices, powerful multisigs can tighten access controls — meaning the multisigs can immediately block access to price feeds at will.

This is not limited to oracles. Any external dependency — a DEX pool, a staking contract, an AMM price curve — that a protocol hard-requires for a critical path is a potential integration DoS vector.

Vulnerable Code

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

import "@chainlink/contracts/src/v0.8/interfaces/AggregatorV3Interface.sol";

contract VulnerableLending {
    AggregatorV3Interface public immutable priceFeed;
    mapping(address => uint256) public collateral;

    constructor(address _feed) {
        priceFeed = AggregatorV3Interface(_feed);
    }

    // ❌ Direct call — if Chainlink reverts for ANY reason, all borrows revert
    function borrow(uint256 usdAmount) external {
        (, int256 price, , , ) = priceFeed.latestRoundData(); // can revert
        require(price > 0, "Invalid price");

        uint256 ethRequired = (usdAmount * 1e18) / uint256(price);
        require(collateral[msg.sender] >= ethRequired * 150 / 100, "Undercollateralized");

        // ... issue loan
    }
}

Attack Scenario

The Chainlink multisig pauses the feed (governance action, sequencer outage, or deliberate griefing by an authorized party). Every call to borrow() reverts. The lending protocol is frozen — users cannot borrow, and depending on the architecture, they may not be able to repay either if the same feed gates repayments.

Correct Design

To prevent denial of service scenarios, it is recommended to query Chainlink price feeds using a defensive approach with Solidity’s try/catch structure. In this way, if the call to the price feed fails, the caller contract is still in control and can handle any errors safely and explicitly. In a scenario where the call reverts, the catch block can be used to explicitly revert, call a fallback oracle, or handle the error in any way suitable for the contract’s logic.

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

import "@chainlink/contracts/src/v0.8/interfaces/AggregatorV3Interface.sol";

interface IFallbackOracle {
    function getPrice() external view returns (uint256);
}

contract SafeLending {
    AggregatorV3Interface public immutable primaryFeed;
    IFallbackOracle       public immutable fallbackOracle;

    uint256 public constant STALE_THRESHOLD = 1 hours;

    constructor(address _primary, address _fallback) {
        primaryFeed     = AggregatorV3Interface(_primary);
        fallbackOracle  = IFallbackOracle(_fallback);
    }

    // ✅ try/catch insulates the protocol from primary oracle failure
    function _getPrice() internal view returns (uint256 price) {
        try primaryFeed.latestRoundData() returns (
            uint80,
            int256 answer,
            uint256,
            uint256 updatedAt,
            uint80
        ) {
            require(answer > 0, "Negative price");
            require(block.timestamp - updatedAt <= STALE_THRESHOLD, "Stale price");
            price = uint256(answer);
        } catch {
            // Primary failed — fall back to secondary oracle
            price = fallbackOracle.getPrice();
        }
    }

    function borrow(uint256 usdAmount) external {
        uint256 ethPrice = _getPrice();
        uint256 ethRequired = (usdAmount * 1e18) / ethPrice;
        // ... collateral check and loan issuance
    }
}

Always validate staleness (updatedAt), negativity of the returned price, and have at least one fallback data source. Circuit-breaker patterns (pausing the protocol if both oracles fail) are preferable to silent wrong-price scenarios.


7. Governance DoS (Preventing Quorum from Being Reached)

Why It Happens

If no action could be taken unless 100% of token holders voted, it is very likely nothing would ever be accomplished — the system would grind to a halt if only one token holder decided not to participate. On the other hand, if only 1% of votes were required, it would be too easy to pass undesirable proposals. For the fate of a proposal to be decided, it must reach a quorum threshold (a percentage of the total possible votes) within the voting period.

Governance DoS takes two forms: the suppression attack (preventing quorum from ever being reached) and the inflation attack (diluting quorum by inflating total supply). Low participation is the norm — most DAOs see 5–15% voter turnout. When quorum is set at 10% of circulating supply and 85% of tokens are dormant, an attacker needs to outbid nobody.

Vulnerable Code

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

import "@openzeppelin/contracts/token/ERC20/IERC20.sol";

contract VulnerableGovernor {
    IERC20 public govToken;
    uint256 public totalSupply;

    struct Proposal {
        string  description;
        uint256 votesFor;
        uint256 votesAgainst;
        uint256 deadline;
        bool    executed;
    }

    Proposal[] public proposals;

    // ❌ Quorum is a fixed fraction of totalSupply, which can be inflated
    uint256 public quorumNumerator   = 10; // 10%
    uint256 public quorumDenominator = 100;

    // ❌ No snapshot: voters can transfer tokens after voting to vote again
    mapping(uint256 => mapping(address => bool)) public hasVoted;

    function propose(string calldata desc) external returns (uint256) {
        proposals.push(Proposal(desc, 0, 0, block.timestamp + 3 days, false));
        return proposals.length - 1;
    }

    function vote(uint256 proposalId, bool support) external {
        Proposal storage p = proposals[proposalId];
        require(block.timestamp < p.deadline, "Voting closed");
        require(!hasVoted[proposalId][msg.sender], "Already voted");

        // ❌ Live balance — not a snapshot
        uint256 weight = govToken.balanceOf(msg.sender);
        hasVoted[proposalId][msg.sender] = true;

        if (support) p.votesFor += weight;
        else         p.votesAgainst += weight;
    }

    function execute(uint256 proposalId) external {
        Proposal storage p = proposals[proposalId];
        require(block.timestamp >= p.deadline, "Voting not ended");
        require(!p.executed, "Already executed");

        // ❌ Quorum denominator is live totalSupply — inflatable
        uint256 quorum = (govToken.totalSupply() * quorumNumerator) / quorumDenominator;
        uint256 totalVotes = p.votesFor + p.votesAgainst;
        require(totalVotes >= quorum, "Quorum not reached");

        p.executed = true;
        // ... execute proposal
    }
}

Attack Scenarios

Suppression attack: A whale holding 20% of tokens never votes. Combined with chronic low turnout, legitimate proposals never hit quorum. The protocol is governance-frozen. This is the “apathy exploit.” If a governance system has inadequate minimum quorum thresholds and only 4–5% of total token holders historically vote, an attacker needs only a slightly larger fraction of circulating supply to become a dictator. Setting a 1-minute timelock, 0.01% quorum, or skipping the proposal threshold defeats the purpose entirely.

Inflation attack: The attacker mints (or buys) a large supply of governance tokens after a proposal is live, inflating totalSupply. Because quorum is computed against live supply, existing votes may never satisfy the new denominator.

Double-vote attack: Without a snapshot, the attacker votes, transfers their tokens to a second address, and votes again, artificially suppressing or inflating results.

Correct Design

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

// Uses OpenZeppelin's ERC20Votes — every transfer updates checkpoints
import "@openzeppelin/contracts/token/ERC20/extensions/ERC20Votes.sol";

contract SafeGovernor {
    ERC20Votes public immutable govToken;

    struct Proposal {
        string  description;
        uint256 votesFor;
        uint256 votesAgainst;
        uint256 snapshotBlock;   // ✅ Fixed at
 block.number; // snapshot in the past

    mapping(uint256 => Proposal) public proposals;
    uint256 public nextId;

    function propose(string calldata description) external returns (uint256 id) {
        require(
            token.getPastVotes(msg.sender, block.number - 1) >= PROPOSAL_THRESHOLD,
            "insufficient votes"
        );
        id = nextId++;
        proposals[id] = Proposal({
            description: description,
            votesFor: 0,
            votesAgainst: 0,
            snapshotBlock: block.number - 1
        });
    }

    function vote(uint256 proposalId, bool support) external {
        Proposal storage p = proposals[proposalId];
        uint256 weight = token.getPastVotes(msg.sender, p.snapshotBlock);
        require(weight > 0, "no votes at snapshot");
        if (support) p.votesFor += weight;
        else p.againstVotes += weight;
    }
}

Denial-of-Service Audit Checklist

Gas and loop safety

  • No unbounded loop over user-controlled or ever-growing data structures
  • No external call inside a loop that can cause the entire batch to revert
  • Gas consumption per operation is bounded and documented

Pull over push

  • ETH and token distributions use pull payment (claimable) rather than push (auto-send)
  • Failed individual claims do not block other users’ claims
  • No function sends ETH to multiple recipients in a single loop

Governance and voting

  • Proposal creation requires a meaningful token threshold to prevent spam
  • Proposal limit (active at one time) is bounded
  • Voting power snapshot is taken at a past block so flash loans cannot influence it

Reentrancy and callback DoS

  • No function trusts an external callback to complete in order to finalize state
  • Callback failures are caught and handled — they do not brick the calling function
  • ERC-777 and ERC-1155 tokens are identified; their hooks cannot DoS withdrawal flows

Liquidity and AMM

  • Pool operations cannot be permanently blocked by a dust liquidity position
  • Minimum deposit / minimum liquidity thresholds prevent griefing with tiny amounts
  • Locked liquidity is not required by any function that must succeed unconditionally