Skip to content
Request an audit

Gas consumed in notifyUnsubscribe is underestimated during tests and is greater than 300,000 without pre-warming

TitleGas consumed in notifyUnsubscribe is underestimated during tests and is greater than 300,000 without pre-warming
Reward$2613, Unique
ContestBMX - 2 Sep 2025 on Sherlock
Authorcergyk
ContextOut of Gas

To protect users against bricked positions by malicious subscriber contracts, Uniswap V4 notifier allows recovery of a reverting notifyUnsubscribe call:

solidity
if (address(_subscriber).code.length > 0) {
    // require that the remaining gas is sufficient to notify the subscriber
    // otherwise, users can select a gas limit where .notifyUnsubscribe hits OutOfGas yet the
    // transaction/unsubscription can still succeed
    if (gasleft() < unsubscribeGasLimit) GasLimitTooLow.selector.revertWith();
    //@audit try/catch to protect against malicious _subscriber
    try _subscriber.notifyUnsubscribe{gas: unsubscribeGasLimit}(tokenId) {} catch {}
}

Conversely, to protect the subscriber against a forced failure caused by an insufficient user gas limit, the outer call reverts if less than unsubscribeGasLimit is available. That guarantees at least 6364 · unsubscribeGasLimit reaches the notifyUnsubscribe call, the factor coming from EIP-150.

On Base, unsubscribeGasLimit is set to 300,000.

So PositionManagerAdapter.notifyUnsubscribe must keep its gas cost under 300000 · 6364 ≈ 295000, or users can unsubscribe successfully on Uniswap V4 while leaving positions with liquidity in the Deli gauge accounting.

Unfortunately, this is not the case, and the call to notifyUnsubscribe can consume more than 300,000 gas.

Users can force unsubscribe from Uniswap V4, but keep positions accounting intact in Deli gauges.

The malicious user can then burn or immediately resubscribe the position to PositionManagerAdapter, diluting the rewards for that range by the position's liquidity forever. Even if the admin calls adminForceUnsubscribe, the liquidity stays in the global gauge accounting.

solidity
poolRewards[pid].modifyPositionLiquidity(
    RangePool.ModifyLiquidityParams({
        tickLower: tickLower,
        tickUpper: tickUpper,
        //@audit when subscribing, current position liquidity is always added to global liquidity tracking
        liquidityDelta: SafeCast.toInt128(uint256(liquidity)),
        tickSpacing: poolTickSpacing[pid]
    }),
    toks
);

Alpha: on Base, the notifier forwards at most 300k gas to the hook, and execution continues if the hook reverts or runs out of gas — leaving the position detached from it.

Conclusion

This finding would earn you $2613. Check that the notifier hook cannot run out of gas, since that reverts every state change the hook was supposed to make.

Full Report
Codebase

Message on Telegram All 62 posts