> For the complete documentation index, see [llms.txt](https://docs-v4.nftx.io/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs-v4.nftx.io/contracts/nftx-flex-hook.md).

# NFTX Flex Hook

The second Uniswap V4 hook — permissionless pools pairing a collection token with any whitelisted pair token, sharing the canonical hook's fee logic.

`NFTXFlexHook` runs the [flex pools](/how-it-works/flex-pools.md): Uniswap V4 pools that pair a collection token registered on the [Locker](/contracts/locker.md) with a pair token the [pair-token whitelist](#pair-token-whitelist) accepts. It is a separate deployment beside the canonical [NFTX V4 Hook](/contracts/nftx-v4-hook.md), which is unchanged: the Locker still creates every collection's flETH pool through the canonical hook, and every existing pool key, position and fee balance stays where it is.

Both hooks are thin subclasses of one abstract core, `NFTXHookCore`, which holds the swap, fee, donation, oracle and escrow logic they share. The flex hook adds permissionless initialisation, the whitelist pointer, a pause switch, a `deregisterPool` control and native-ETH settlement; it has none of the canonical hook's Locker-only lifecycle functions (`registerCollection`, `initializeCollection`, `depositFees`, `getCollectionPoolKey`, `getCollectionFee`). Fees are in **Uniswap V4 pip units**, where `1_000_000` equals 100%.

Signatures below come from `INFTXFlexHook`, which extends `INFTXHookCore`. Every flex event is keyed by `PoolId`, because a flex pool is not one-per-collection.

## Pool initialisation

There is no function to call on the hook. A flex pool is created by `PoolManager.initialize(key, sqrtPriceX96)` with a `PoolKey` whose `hooks` is this contract; the hook's `beforeInitialize` callback admits or refuses it. Checks run in this order, each with its own error:

| #  | Rule                                                    | Error                                                               |
| -- | ------------------------------------------------------- | ------------------------------------------------------------------- |
| 1  | Pool creation is not paused                             | `PoolCreationPaused()`                                              |
| 2  | `key.fee` is the dynamic-fee flag (`0x800000`)          | `StaticFeeNotSupported(uint24 _fee)`                                |
| 3  | `key.tickSpacing` is 10, 60 or 200                      | `TickSpacingNotAllowed(int24 _tickSpacing)`                         |
| 4a | At least one currency passes `Locker.isCollectionToken` | `NoCollectionToken(address _currency0, address _currency1)`         |
| 4b | Not both currencies do                                  | `BothSidesCollectionTokens(address _currency0, address _currency1)` |
| 5  | Neither currency is the canonical pair token (flETH)    | `CanonicalPairTokenNotAllowed()`                                    |
| 6  | The pool id was not previously deregistered             | `PoolDeregistered()`                                                |
| 7  | The whitelist allows `(collectionToken, pairToken)`     | `PairTokenNotAllowed(address _pairToken)`                           |

Native ETH is `address(0)`, which never passes `isCollectionToken`, so a native pool always has the collection token as `currency1`. Whether `address(0)` is an acceptable pair is the whitelist's decision, not the hook's; a revert inside the whitelist bubbles up and simply fails that initialisation.

On success the hook stores the pool's parameters (`poolParams(poolId).collection` is the **collection token** address here — on the canonical hook the same field holds the ERC-721 collection), seeds the price oracle at the initial tick, and emits `FlexPoolInitialized`. No state event is emitted at this point because `slot0` is not written yet; the first liquidity add emits the first `FlexPoolStateUpdated`.

{% hint style="warning" %}
`PositionManager.initializePool` swallows hook reverts and returns `type(int24).max`, so a refused key looks like a success to a script that only checks the call. Either call `PoolManager.initialize` directly, or read `poolParams(poolId).initialized` afterwards.
{% endhint %}

## Views

| Function                                         | Returns                         | Description                                                                                                              |
| ------------------------------------------------ | ------------------------------- | ------------------------------------------------------------------------------------------------------------------------ |
| `nativeToken()`                                  | `address`                       | The canonical pair token (flETH). Pools against it are rejected here                                                     |
| `pairTokenWhitelist()`                           | `IPairTokenWhitelist`           | The policy consulted at initialisation                                                                                   |
| `paused()`                                       | `bool`                          | Whether new pool creation is paused. Existing pools keep trading                                                         |
| `isAllowedTickSpacing(int24)`                    | `bool`                          | `true` for 10, 60 and 200                                                                                                |
| `locker()`                                       | `ILocker`                       | The Locker whose `isCollectionToken` gates admission                                                                     |
| `poolParams(PoolId)`                             | `PoolParams`                    | Recorded parameters: `collection` (the collection token), `initialized`, `currencyFlipped`, `deregistered`, per-pool fee |
| `getFee(PoolId, address _origin)`                | `uint24`                        | The dynamic LP fee for a swap from `_origin`, applying any exemption                                                     |
| `defaultFee()` / `ammFee()` / `ammBeneficiary()` | `uint24` / `uint24` / `address` | The fee settings shared with the canonical hook's model                                                                  |
| `getHookPermissions()`                           | `Hooks.Permissions`             | The same eight callbacks as the canonical hook                                                                           |

## Admin (owner only)

```solidity
function setPairTokenWhitelist(IPairTokenWhitelist _whitelist) external;
function setPaused(bool _paused) external;
function deregisterPool(PoolId _poolId) external;
```

| Function                | Description                                                                                                                                                                                                                                                                                                                                                                                                      |
| ----------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `setPairTokenWhitelist` | Points the hook at a new whitelist. Takes effect for the **next** initialisation; existing pools are never closed by a policy change. Reverts `ZeroAddress()` for the zero address. Emits `PairTokenWhitelistSet`                                                                                                                                                                                                |
| `setPaused`             | Pauses or resumes new pool creation. Emits `PausedSet`                                                                                                                                                                                                                                                                                                                                                           |
| `deregisterPool`        | Retires a pool. The Locker's sunset never clears `isCollectionToken`, so a wound-down collection's flex pools would otherwise keep quoting an unredeemable token. After this, swaps revert `PoolDeregistered()`, liquidity callbacks stay open so LPs can exit, and the key can never be re-admitted. Terminal. Reverts `PoolNotInitialized()` for a pool this hook never admitted. Emits `FlexPoolDeregistered` |

The fee controls inherited from the shared core — `setDefaultFee`, `setFee` (per pool), `setAmmFee`, `setAmmBeneficiary`, `setFeeExemption`, `removeFeeExemption`, `setNotifier` — are documented on the [NFTX V4 Hook](/contracts/nftx-v4-hook.md#fee-configuration-owner-only) page and behave the same here. Fee exemptions are per hook: an exemption granted on the canonical hook does not apply to flex pools unless it is set here too.

## Fees and native ETH

The AMM fee is taken from the unspecified leg of every swap, in whatever currency that leg is in, and credited to the AMM beneficiary through the hook's built-in token escrow, where the beneficiary withdraws it. Two things differ from the canonical hook:

* **The escrow credit is measured, not assumed.** The flex hook credits the balance the swap actually delivered, capped at the nominal fee, so a pair token that charges a transfer fee or rebases cannot leave the escrow short. A pair token that blocklists the hook makes that pool's swaps revert and touches no other pool.
* **Native ETH settles.** The hook has a `receive()` function so the `PoolManager` can deliver an ETH fee, which the escrow tracks as `address(0)`. Stray ETH sent to the hook is unaccounted and cannot be drained; the same is true of stray ERC-20s on either hook.

Flex pools have no `depositFees`, so the listing-tax donations that [Listings](/contracts/listings.md) routes into canonical pools never reach them.

## Events

Flex-specific, all keyed by `PoolId`:

```solidity
event FlexPoolInitialized(PoolId indexed _poolId, address indexed _collectionToken, address indexed _pairToken, PoolKey _key, uint160 _sqrtPriceX96);
event FlexPoolStateUpdated(PoolId indexed _poolId, uint160 _sqrtPriceX96, int24 _tick, uint24 _protocolFee, uint24 _swapFee, uint128 _liquidity);
event FlexPoolFeeSet(PoolId indexed _poolId, uint24 _fee);
event FlexPoolFeesDonated(PoolId indexed _poolId, uint _amount0, uint _amount1); // unreachable in v1: no depositFees
event FlexAMMFeesTaken(PoolId indexed _poolId, address _recipient, address _token, uint _amount);
event FlexPoolDeregistered(PoolId indexed _poolId);
event PausedSet(bool _paused);
event PairTokenWhitelistSet(address _whitelist);
```

Shared with the canonical hook through `INFTXHookCore`: `DefaultFeeSet`, `AMMFeeSet`, `AMMBeneficiarySet`, `FeeOverrideSet`, `FeeOverrideRemoved`, `NotifierSet`.

## Pair-token whitelist

`IPairTokenWhitelist` is a one-function policy contract the hook consults once, at initialisation:

```solidity
function isPairTokenAllowed(address _collectionToken, address _pairToken) external view returns (bool);
```

Both sides are passed so a later version can whitelist per collection without an interface change. Native ETH is `address(0)`.

The first implementation, `OpenPairTokenWhitelist`, is stateless and ownerless and returns `true` for every pair, `address(0)` included. Policy changes are a new whitelist deployment adopted through `setPairTokenWhitelist`, never an edit to the deployed one. Because the whitelist is only consulted at initialisation, delisting a pair token later stops **new** pools with it and never closes an existing one.

## Deployment

The flex hook and the whitelist are deployed through CreateX's CREATE3 from the protocol deployer under the `nftx.v3` salt namespace, so — like every other protocol contract — they will carry the same address on each chain they reach. The hook's salt is mined so its address carries the eight hook-permission flags.

| Field                    | Value                                                                                  |
| ------------------------ | -------------------------------------------------------------------------------------- |
| `NFTXFlexHook`           | `0xC26A5Cb51b1818F62a4C6693a9a1feDB3340efc4`                                           |
| `OpenPairTokenWhitelist` | `0x0A97DDb5d5B96ed0E9E014ced7bC7AD20b8Df494`                                           |
| `NFTXFlexHook` salt      | `0xb8a70b4d1547bf6193bd67a73f4f98ea9fd0a97300d5058ee1744140af470ce5` (iteration 14037) |

Ethereum Mainnet is live (block `25933487`, 2026-09-08) with the canonical hook's fee settings copied at deployment — default LP fee `9000` (0.9%), AMM fee `1000` (0.1%), AMM beneficiary the `ProtocolFeeReceiver` — and the open whitelist attached. Ownership sits with the protocol's Labs multisig, as it does for the Locker and the canonical hook. The full per-chain list is on [Deployment Addresses](/contracts/addresses.md).

Base Sepolia is excluded: its earlier-generation Locker has no `isCollectionToken`, which the flex hook needs. Base Mainnet was redeployed onto the current generation on 18 September 2026 and its Locker does have it, so nothing rules the flex hook out there — it has simply not been deployed to Base yet.
