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

Bug Deep Dive #25 · 21 December 2025

Bug Deep Dives · [The Contest Academy](https://0xsimao.com/the-contest-academy) · 0xSimao

---

| 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:

```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 $\frac{63}{64} \cdot \text{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 $\frac{300000 \cdot 63}{64} \approx 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**](https://audits.sherlock.xyz/contests/1154/report)\
[**Codebase**](https://github.com/morphex-labs/deli-swap-contracts)

---

Newer: [Vault owner is incentivized to not switch invalid collateral token during liquidation](https://0xsimao.com/the-contest-academy/bug-deep-dive-26) · Older: [ViewFacet.queryAssetBalances doesn't include uncollected uniswap fees for compounded maker position](https://0xsimao.com/the-contest-academy/bug-deep-dive-24)
