Gas consumed in notifyUnsubscribe is underestimated during tests and is greater than 300,000 without pre-warming
| Title | Gas consumed in notifyUnsubscribe is underestimated during tests and is greater than 300,000 without pre-warming |
|---|---|
| Reward | $2613, Unique |
| Contest | BMX - 2 Sep 2025 on Sherlock |
| Author | cergyk |
| Context | Out of Gas |
To protect users against bricked positions by malicious subscriber contracts, Uniswap V4 notifier allows recovery of a reverting notifyUnsubscribe call:
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.
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.