<!-- canonical: https://0xsimao.com/findings/bmx-voter-unbounded-removals-lacking -->

# DoSed `Voter::finalize()` due to unbounded pending removals lacking a batch argument variable

Crit/High · Sherlock · Perpetuals DEX · 2nd September 2025

Finding H-3 of the BMX competition.

- Protocol: https://audits.sherlock.xyz/contests/1154
- Source: https://github.com/sherlock-audit/2025-09-bmx-deli-swap-judging/issues/68

---

### Summary

`Voter::finalize()` processes in batches to allow finalizing epochs and distributing rewards without running out of gas. However, `Voter::_processPendingRemovals()` doesn't take a batch variable into account, making it possible for an attacker to register as auto voter with a balance > 0, and then transfer their votes out, being sent to the pending removals array as soon as the epoch is finalized. By spamming this in a huge number of accounts, an attacker is able to DoS epoch finalization for all epochs, effectively making all rewards stuck.

### Root Cause


In `Voter.sol:386`, [Voter::_processPendingRemovals()](https://github.com/sherlock-audit/2025-09-bmx-deli-swap/blob/main/deli-swap-contracts/src/Voter.sol#L386) is missing a batch variable argument.

### Internal Pre-conditions

None

### External Pre-conditions

None

### Attack Path

1. Attacker calls `Voter::vote()` with a wei balance, enabling auto voting, and then transferring the balance out. Attacker does this for multipler addresses such that `Voter::finalize()` runs out of gas.
2. Admin calls `Voter::finalize()`, and in `Voter::_tallyAutoVotes()`, all the accounts are pushed to the pendingRemovals array. Due to the lack of a batch argument in the `Voter::_processPendingRemovals()` function, it will run out of gas trying to remove all the addresses of the attacker.

### Impact

Epoch finalization is impossible and rewards are forever stuck.

### PoC

Note `Voter.sol::_processPendingRemovals()`:

```solidity
function _processPendingRemovals(uint256 ep) internal {
    address[] memory toRemove = pendingRemovals[ep];
    for (uint256 i = 0; i < toRemove.length; i++) {
        address addr = toRemove[i];
        uint256 idx = autoIndex[addr];
        if (idx > 0) {
            _removeAutoVoter(addr, idx - 1);
        }
    }
    delete pendingRemovals[ep];
}
```

### Mitigation

Send a batch argument, to avoid OOG revert.

---

Related findings:

- [Don't return the same memory variable if your passing it as argument](https://0xsimao.com/findings/fuji-finance-memory-your-passing-argument): Fuji Finance
- [Batch:withdraw() can be DoSed by frontrunning it with strategyRouter:allocateToStrategies()](https://0xsimao.com/findings/clip-finance-i-withdraw-frontrunning-allocate-strategies): Clip Finance Strategies
- [BatchOut:withdrawFulfill() can be DoSed by spamming withdrawal requests, leading to OOG reverts](https://0xsimao.com/findings/clip-finance-i-withdraw-spamming-withdrawal-reverts): Clip Finance Strategies
