Skip to content

Conversation

@monsieur-mansa
Copy link

Summary

This PR adds the PermaLockHook contract to the hooksAddressesAllowlist.ts file so that routing through pools using this hook is enabled on Monad.

Hook purpose:
PermaLockHook is a minimal v4 hook that permits fee collection while permanently disabling liquidity withdrawal. Liquidity providers can add liquidity and collect fees, but attempts to call removeLiquidity() are intentionally reverted.


Contract Details


Safety Considerations

PermaLockHook follows a very limited and deterministic design:

  1. No external calls → avoids reentrancy risk.
  2. No token transfers, no accounting changes → fees and swaps behave as expected.
  3. Single responsibility: only overrides the beforeRemoveLiquidity hook to revert liquidity removal.
  4. No unbounded loops or dynamic logic → predictable gas usage.
  5. Does not alter swap execution and should not impact routing correctness.

Because behavior is restricted to reverting removeLiquidity(), the hook cannot take custody, move tokens, or alter balances.


Rationale for Allowlist Inclusion

Create non-withdrawable liquidity pools, which improve launch safety by ensuring LP positions cannot be rugged.
Allowlisting enables Uniswap routing to interact with these pools correctly.


Checklist

  • Hook deployed on Monad
  • Source verified
  • Deterministic + gas-bounded behavior
  • No effect on swap correctness
  • Only intercepts beforeRemoveLiquidity

@monsieur-mansa
Copy link
Author

Source code posted below for convenience.

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.26;

import {BaseHook} from "@openzeppelin/uniswap-hooks/src/base/BaseHook.sol";

import {Hooks} from "@uniswap/v4-core/src/libraries/Hooks.sol";
import {IPoolManager, ModifyLiquidityParams} from "@uniswap/v4-core/src/interfaces/IPoolManager.sol";
import {PoolKey} from "@uniswap/v4-core/src/types/PoolKey.sol";

contract PermaLockHook is BaseHook {
    error LiquidityLocked();

    constructor(IPoolManager _poolManager) BaseHook(_poolManager) {}

    function getHookPermissions() public pure override returns (Hooks.Permissions memory) {
        return Hooks.Permissions({
            beforeInitialize: false,
            afterInitialize: false,
            beforeAddLiquidity: false,
            afterAddLiquidity: false,
            beforeRemoveLiquidity: true,
            afterRemoveLiquidity: false,
            beforeSwap: false,
            afterSwap: false,
            beforeDonate: false,
            afterDonate: false,
            beforeSwapReturnDelta: false,
            afterSwapReturnDelta: false,
            afterAddLiquidityReturnDelta: false,
            afterRemoveLiquidityReturnDelta: false
        });
    }

    // -----------------------------------------------
    // NOTE: see IHooks.sol for function documentation
    // -----------------------------------------------

    function _beforeRemoveLiquidity(address, PoolKey calldata, ModifyLiquidityParams calldata params, bytes calldata)
        internal
        pure
        override
        returns (bytes4)
    {
        // Allow fee collection (liquidityDelta == 0) but block actual liquidity removal (liquidityDelta < 0)
        if (params.liquidityDelta < 0) {
            revert LiquidityLocked();
        }
        return this.beforeRemoveLiquidity.selector;
    }
}

@Ponx
Copy link
Contributor

Ponx commented Dec 24, 2025

@monsieur-mansa have you created a pool with this hook yet? If not, could you do so and link the pool here?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants