<!-- canonical: https://0xsimao.com/findings/bmx-voter-rewards-transfering-weth -->

# `Voter::finalize()` incorrect rewards distribution due to transfering WETH before calling `distributor::setTokensPerInterval()`

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

Finding H-8 of the BMX competition.

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

---

### Summary

`Voter::finalize()` transfers the WETH, and then calls `distributor.setTokensPerInterval()`. However, this order of operations is incorrect and it will lead to the previous reward rate being applied for the transferred WETH, doing an instantaneous rewards increase that will be arbitraged and stolen.

As can be seen below, `RewardsDistributor::setTokensPerInterval()` calls `RewardTracker::updateRewards()`, which calls `RewardsDistributor::distribute()`. Now, if `pendingRewards()` will apply the last `tokensPerInterval`, accrue for the last time called, and send to users if there is enough balance. Due to having sent the WETH before calling `setTokensPerInterval()`, it will instantly distribute this rewards as soon as the function is called, and there won't be enough rewards for the next week. Thus, past stakers (and bots or attackers that knew about this) will steal instant rewards, and honest stakers will not collect them over the full week.

[**RewardsDistributor**](https://basescan.org/address/0x0259083181ae54730f4fbb1c174a53e21bce5266#code)
```solidity
    function setTokensPerInterval(uint256 _amount) external onlyAdmin {
        require(lastDistributionTime != 0, "RewardDistributor: invalid lastDistributionTime");
        IRewardTracker(rewardTracker).updateRewards();
        tokensPerInterval = _amount;
        emit TokensPerIntervalChange(_amount);
    }

    function pendingRewards() public view override returns (uint256) {
        if (block.timestamp == lastDistributionTime) {
            return 0;
        }

        uint256 timeDiff = block.timestamp.sub(lastDistributionTime);
        return tokensPerInterval.mul(timeDiff);
    }

    function distribute() external override returns (uint256) {
        require(msg.sender == rewardTracker, "RewardDistributor: invalid msg.sender");
        uint256 amount = pendingRewards();
        if (amount == 0) { return 0; }

        lastDistributionTime = block.timestamp;

        uint256 balance = IERC20(rewardToken).balanceOf(address(this));
        if (amount > balance) { amount = balance; }

        IERC20(rewardToken).safeTransfer(msg.sender, amount);

        emit Distribute(amount);
        return amount;
    }
```

[**RewardTracker**](https://basescan.org/address/0x38E5be3501687500E6338217276069d16178077E#code)
```solidity
    function updateRewards() external override nonReentrant {
        _updateRewards(address(0));
    }

    function _updateRewards(address _account) private {
        uint256 blockReward = IRewardDistributor(distributor).distribute();
    ...
```

### Root Cause

In `Voter.sol:262`, it transfers WETH before calling [setTokensPerInterval()](https://github.com/sherlock-audit/2025-09-bmx-deli-swap/blob/main/deli-swap-contracts/src/Voter.sol#L263).

### Internal Pre-conditions

None

### External Pre-conditions

None

### Attack Path

1. Admin finalizes an epoch, sends WETH and sets the tokens per interval to distribute this amount of WETH over 1 week.
2. Due to the logic above, part of this WETH, if not all, will be consumed instantly and rewards won't be distributed over the full week.

### Impact

Stolen rewards.

### PoC

_No response_

### Mitigation

First call setTokensPerInterval(), then transfer the WETH.

---

Related findings:

- [Estimated prize draws in TieredLiquidityDistributor are off due to rounding down when calculating the sum, leading to incorrect prizes](https://0xsimao.com/findings/pooltogether-the-prize-layer-for-defi-estimated-draws-tiered-rounding): PoolTogether Prize Layer
- [Wrong reward distribution between early and late depositors because of the late `syncRewards()` call in the cycle, `syncReward()` logic should be executed in each withdraw or deposits (without reverting)](https://0xsimao.com/findings/gogopool-reward-rewards-withdraw-deposits): GoGoPool
- [StakingPool is missing rewards distribution end logic](https://0xsimao.com/findings/spirit-protocol-staking-rewards-distribution-end): Spirit Protocol
- [The incorrect accounting of protocol fee will cause double charging fee and wrong distribution of earnings for variable users](https://0xsimao.com/findings/saffron-lido-vaults-accounting-fee-double-charging): Saffron Lido Vaults
