Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
531aeb0
feat: turn onSwap into onAfterSwap, add onBeforeSwap
clemlak Feb 12, 2026
73256dd
Merge branch 'main' into feat/rehype-decaying-fee
clemlak Mar 2, 2026
d7b6bf0
feat: add decaying fee to RehypeHook
clemlak Mar 2, 2026
e994244
fix: reentrancy in collectFees
clemlak Mar 4, 2026
e930f43
chore: remove check for impossible case
clemlak Mar 4, 2026
964aa64
fix: roll back Decaying fee changes to the Rehype Migrator
clemlak Mar 4, 2026
29d5b2b
chore: remove unused code
clemlak Mar 5, 2026
9ebb95e
chore: remove unused code, read LP fee from PoolManager
clemlak Mar 5, 2026
1174a10
chore: remove unused code, add MigratorInitData
clemlak Mar 5, 2026
b9d131c
test: update various tests
clemlak Mar 5, 2026
636f5f5
fix: use custom fee again for Rehype Doppler Hook Migrator
clemlak Mar 5, 2026
4995269
test: add more tests
clemlak Mar 5, 2026
9cbe6cb
chore: rename BaseDopplerHook to BaseDopplerHookInitializer
clemlak Mar 5, 2026
38392d9
chore: roll back change
clemlak Mar 5, 2026
b75e29c
chore: remove unused field
clemlak Mar 5, 2026
f1e7187
test: add more tests
clemlak Mar 5, 2026
4b89717
chore: fmt
clemlak Mar 5, 2026
5a97241
chore: update DopplerHookInitializer docs
clemlak Mar 6, 2026
47ea129
build: update DopplerHookInitializer mining script
clemlak Mar 6, 2026
e03252e
chore: update docs, deploying scripts
clemlak Mar 6, 2026
33ff1da
fix: stack too deep (no idea why)
clemlak Mar 6, 2026
e51993e
feat: add BaseMinimalHook
clemlak Mar 6, 2026
8f58d2a
test: add new tests
clemlak Mar 9, 2026
28aaccf
build: deploy contracts
clemlak Mar 9, 2026
779f4ed
feat: Rehype Initializer Decay, fix fees stuff in Rehype Migrator
clemlak Mar 9, 2026
bf4ba0e
chore: update docs
clemlak Mar 9, 2026
fad4c28
test: add new tests
clemlak Mar 9, 2026
f92d5f2
chore: rename contracts and clean up code
clemlak Mar 9, 2026
278d6c4
chore: rename script, update deployment salts
clemlak Mar 9, 2026
21d3484
build: roll back deployments
clemlak Mar 9, 2026
f46832d
test: rename file
clemlak Mar 9, 2026
9e1d24c
feat: update MAX_SWAP_FEE
clemlak Mar 9, 2026
12407ac
build: deploy RehypeDopplerHookInitializer and RehypeDopplerHookMigra…
clemlak Mar 9, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
369 changes: 160 additions & 209 deletions broadcast/multi/DeployDopplerHookInitializer.s.sol-latest/run.json

Large diffs are not rendered by default.

314 changes: 157 additions & 157 deletions broadcast/multi/DeployRehypeDopplerHook.s.sol-latest/run.json

Large diffs are not rendered by default.

Large diffs are not rendered by default.

Large diffs are not rendered by default.

Large diffs are not rendered by default.

314 changes: 157 additions & 157 deletions broadcast/multi/DeployRehypeDopplerHookMigrator.s.sol-latest/run.json

Large diffs are not rendered by default.

30 changes: 15 additions & 15 deletions deployments.config.toml
Original file line number Diff line number Diff line change
Expand Up @@ -25,13 +25,13 @@ uniswap_v4_migrator = "0x0820A4D0173C17Ece283f7bDaAF0f8876eB205f5"
uniswap_v4_migrator_hook = "0x4053D4fa966cbdCC20Ec62070aC8814De8bEE500"
uniswap_v4_initializer = "0x53b4c21a6Cb61D64F636ABBfa6E8E90E6558e8ad"
doppler_deployer = "0xb35469ee64A87Afd19B31615094fE3962d73e421"
doppler_hook_initializer = "0xAA096F558f3d4c9226De77E7Cc05f18E180B2544"
rehype_doppler_hook = "0x3Ec4798A9B11e8243A8Db99687f7A23597B96623"
doppler_hook_initializer = "0x8d2d06Fd273cd19e1cA6d6641e93137F1b0F25C4"
rehype_doppler_hook = "0xC918c6Edb8e0B62B5B73B3F812249a986ba8066d"
clone_derc20_v2_votes_factory = "0x16F5ACB64F4FA17296E942C51d3395aDC318f9e1"
top_up_distributor = "0x435312320C0330B1999746753551CdFbD83aD814"
streamable_fees_locker_v2 = "0xcE3212e6536F33cD6fbFEE265224131353Ca3D47"
doppler_hook_migrator = "0x1E40b0875DDa35f41E15cFB475403859B8c860c4"
rehype_doppler_hook_migrator = "0xC3c9F4cFD1dC0A7837cC4b202B3455B4156a8005"
rehype_doppler_hook_migrator = "0x82d5E22911fbbCB8d3e45812d74eE6203c5824e0"
governance_factory = "0x9F309D79BEe3E8b2f56FaCF74b7195Df176c8F61"

[11155111]
Expand Down Expand Up @@ -61,15 +61,15 @@ uniswap_v4_migrator = "0x0820A4D0173C17Ece283f7bDaAF0f8876eB205f5"
uniswap_v4_migrator_hook = "0x4053D4fa966cbdCC20Ec62070aC8814De8bEE500"
uniswap_v4_initializer = "0x53b4c21a6Cb61D64F636ABBfa6E8E90E6558e8ad"
doppler_deployer = "0xb35469ee64A87Afd19B31615094fE3962d73e421"
doppler_hook_initializer = "0xAA096F558f3d4c9226De77E7Cc05f18E180B2544"
rehype_doppler_hook = "0x3Ec4798A9B11e8243A8Db99687f7A23597B96623"
doppler_hook_initializer = "0x8d2d06Fd273cd19e1cA6d6641e93137F1b0F25C4"
rehype_doppler_hook = "0xC918c6Edb8e0B62B5B73B3F812249a986ba8066d"
clone_derc20_v2_votes_factory = "0x16F5ACB64F4FA17296E942C51d3395aDC318f9e1"
decay_multicurve_initializer_hook = "0xbB7784A4d481184283Ed89619A3e3ed143e1Adc0"
decay_multicurve_initializer = "0xD59cE43E53D69F190E15d9822Fb4540dCcc91178"
top_up_distributor = "0x435312320C0330B1999746753551CdFbD83aD814"
streamable_fees_locker_v2 = "0xcE3212e6536F33cD6fbFEE265224131353Ca3D47"
doppler_hook_migrator = "0x1E40b0875DDa35f41E15cFB475403859B8c860c4"
rehype_doppler_hook_migrator = "0xC3c9F4cFD1dC0A7837cC4b202B3455B4156a8005"
rehype_doppler_hook_migrator = "0x82d5E22911fbbCB8d3e45812d74eE6203c5824e0"
governance_factory = "0x9F309D79BEe3E8b2f56FaCF74b7195Df176c8F61"

[8453]
Expand All @@ -87,8 +87,8 @@ token_factory_80 = "0xf0B5141dD9096254B2ca624dff26024f46087229"
uniswap_v4_pool_manager = "0x498581ff718922c3f8e6a244956af099b2652b2b"
doppler_deployer = "0xb35469ee64A87Afd19B31615094fE3962d73e421"
uniswap_v4_initializer = "0x53b4c21a6Cb61D64F636ABBfa6E8E90E6558e8ad"
doppler_hook_initializer = "0xAA096F558f3d4c9226De77E7Cc05f18E180B2544"
rehype_doppler_hook = "0x3Ec4798A9B11e8243A8Db99687f7A23597B96623"
doppler_hook_initializer = "0x8d2d06Fd273cd19e1cA6d6641e93137F1b0F25C4"
rehype_doppler_hook = "0xC918c6Edb8e0B62B5B73B3F812249a986ba8066d"
no_op_governance_factory = "0x3ad727ee0fbbb8ee0920933fdb96f23fd56f1299"
no_op_migrator = "0x6ddfed58d238ca3195e49d8ac3d4cea6386e5c33"
clone_derc20_v2_votes_factory = "0x16F5ACB64F4FA17296E942C51d3395aDC318f9e1"
Expand All @@ -97,7 +97,7 @@ decay_multicurve_initializer = "0xD59cE43E53D69F190E15d9822Fb4540dCcc91178"
top_up_distributor = "0x435312320C0330B1999746753551CdFbD83aD814"
streamable_fees_locker_v2 = "0xcE3212e6536F33cD6fbFEE265224131353Ca3D47"
doppler_hook_migrator = "0x1E40b0875DDa35f41E15cFB475403859B8c860c4"
rehype_doppler_hook_migrator = "0xC3c9F4cFD1dC0A7837cC4b202B3455B4156a8005"
rehype_doppler_hook_migrator = "0x82d5E22911fbbCB8d3e45812d74eE6203c5824e0"

[130]
endpoint_url = "${UNICHAIN_MAINNET_RPC_URL}"
Expand Down Expand Up @@ -128,16 +128,16 @@ token_factory_80 = "0xf0B5141dD9096254B2ca624dff26024f46087229"
uniswap_v4_pool_manager = "0x188d586ddcf52439676ca21a244753fa19f9ea8e"
doppler_deployer = "0xb35469ee64A87Afd19B31615094fE3962d73e421"
uniswap_v4_initializer = "0x53b4c21a6Cb61D64F636ABBfa6E8E90E6558e8ad"
doppler_hook_initializer = "0xAA096F558f3d4c9226De77E7Cc05f18E180B2544"
rehype_doppler_hook = "0x3Ec4798A9B11e8243A8Db99687f7A23597B96623"
doppler_hook_initializer = "0x8d2d06Fd273cd19e1cA6d6641e93137F1b0F25C4"
rehype_doppler_hook = "0xC918c6Edb8e0B62B5B73B3F812249a986ba8066d"
no_op_governance_factory = "0xb4dee32eb70a5e55f3d2d861f49fb3d79f7a14d9"
no_op_migrator = "0x5f3ba43d44375286296cb85f1ea2ebfa25dde731"
clone_derc20_v2_votes_factory = "0x16F5ACB64F4FA17296E942C51d3395aDC318f9e1"
top_up_distributor = "0x435312320C0330B1999746753551CdFbD83aD814"
airlock_multisig = "0x21E2ce70511e4FE542a97708e89520471DAa7A66"
streamable_fees_locker_v2 = "0xcE3212e6536F33cD6fbFEE265224131353Ca3D47"
doppler_hook_migrator = "0x1E40b0875DDa35f41E15cFB475403859B8c860c4"
rehype_doppler_hook_migrator = "0xC3c9F4cFD1dC0A7837cC4b202B3455B4156a8005"
rehype_doppler_hook_migrator = "0x82d5E22911fbbCB8d3e45812d74eE6203c5824e0"

[4326]
endpoint_url = "${MEGAETH_MAINNET_RPC_URL}"
Expand Down Expand Up @@ -172,8 +172,8 @@ doppler_deployer = "0xb35469ee64A87Afd19B31615094fE3962d73e421"
uniswap_v4_initializer = "0x53b4c21a6Cb61D64F636ABBfa6E8E90E6558e8ad"
uniswap_v4_scheduled_multicurve_hook = "0xc6a562cb5CbFA29BCB1bDCCF903b8B8f2E4A2DC0"
uniswap_v4_scheduled_multicurve_initializer = "0xF84378C9F39e0FF267f3101c88773359c5393876"
doppler_hook_initializer = "0xAA096F558f3d4c9226De77E7Cc05f18E180B2544"
rehype_doppler_hook = "0x3Ec4798A9B11e8243A8Db99687f7A23597B96623"
doppler_hook_initializer = "0x8d2d06Fd273cd19e1cA6d6641e93137F1b0F25C4"
rehype_doppler_hook = "0xC918c6Edb8e0B62B5B73B3F812249a986ba8066d"
no_op_governance_factory = "0x7bd798fafc99a3b17e261f8308a8c11b56935ea1"
no_op_migrator = "0xf11066abbd329ac4bba39455340539322c222eb0"
uniswap_v2_migrator = "0x04a898f3722c38f9def707bd17dc78920efa977c"
Expand All @@ -191,7 +191,7 @@ decay_multicurve_initializer_hook = "0xbB7784A4d481184283Ed89619A3e3ed143e1Adc0"
decay_multicurve_initializer = "0xD59cE43E53D69F190E15d9822Fb4540dCcc91178"
streamable_fees_locker_v2 = "0xcE3212e6536F33cD6fbFEE265224131353Ca3D47"
doppler_hook_migrator = "0x1E40b0875DDa35f41E15cFB475403859B8c860c4"
rehype_doppler_hook_migrator = "0xC3c9F4cFD1dC0A7837cC4b202B3455B4156a8005"
rehype_doppler_hook_migrator = "0x82d5E22911fbbCB8d3e45812d74eE6203c5824e0"

[1301]
endpoint_url = "${UNICHAIN_SEPOLIA_RPC_URL}"
Expand Down
72 changes: 53 additions & 19 deletions docs/DopplerHookInitializer.md
Original file line number Diff line number Diff line change
@@ -1,29 +1,63 @@
# Doppler Hooks
# DopplerHookInitializer

## Overview

Doppler Hooks are a set of callback functions that can be called during the lifecycle of "locked" pools initialized by the `DopplerHookInitializer` contract. Three main events will trigger these hooks:
`DopplerHookInitializer` is a `PoolInitializer` for Uniswap v4 multicurve pools that also acts as the pool's hook contract. In addition to placing and managing liquidity, it can forward lifecycle events to an optional external Doppler Hook contract associated with the pool.

- `initialization`: when a new pool is created
- `swap`: when a swap occurs in the pool
- `graduation`: when the pool reaches a certain price maturity
The external Doppler Hook is configured per asset and can be set during `initialize()` or later via `setDopplerHook()`.

Additionally, pools associated with a Doppler Hook can have their LP fee updated by the associated timelock governance contract or a delegated address.
## Pool Lifecycle

A couple of things to note:
Pools managed by the initializer move through these statuses:

- A pool initialized without a Doppler Hook can opt-in to use one later via the `setHook` function
- A pool initialized with a Doppler Hook can opt-out of using it later by setting the hook address to `address(0)`
- A pool can change its associated Doppler Hook to a different one at any time via the `setHook` function
- Doppler Hooks are approved by the protocol multisig
- A pool without a Doppler Hook cannot be initialized with a dynamic LP fee
| Status | Description |
| --- | --- |
| `Uninitialized` | Default state before `initialize()` is called |
| `Initialized` | Pool exists and liquidity is live, but there are no locked beneficiaries |
| `Locked` | Pool exists and beneficiary accounting is active |
| `Graduated` | Pool has reached its graduation condition and `graduate()` has executed |
| `Exited` | Liquidity has been removed through `exitLiquidity()` |

## Implementation
Only `Locked` pools can change their associated Doppler Hook or graduate.

Here are the different callback functions available for the Doppler Hooks, note that they can be implemented selectively based on the use case:
## Doppler Hook Callbacks

| Callback Function | Triggered By |
| -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------- |
| `onInitialization(address asset, PoolKey calldata key, bytes calldata data` | - `initialize()` if a `dopplerHook` address is set in the `InitData`<br />- `setDopplerHook()` if a Doppler Hook is set after the pool initialization |
| `onSwap(address sender, PoolKey calldata key, IPoolManager.SwapParams calldata params,BalanceDelta delta, bytes calldata data) returns (Currency feeCurrency, int128 hookDelta)` | `afterSwap` before each swap happening in the Uniswap V4 pool |
| `onGraduation(address asset, PoolKey calldata key, bytes calldata data)` | `graduate` if the graduation conditions are met (e.g. `farTick` reached) |
The initializer can forward three callback types to the configured external Doppler Hook. Each callback is enabled independently through `setDopplerHookState()`.

| Callback | Trigger |
| --- | --- |
| `onInitialization(address asset, PoolKey calldata key, bytes calldata data)` | Called during `initialize()` when a hook is already configured, or during `setDopplerHook()` when a new hook is attached to an existing locked pool |
| `onSwap(address sender, PoolKey calldata key, IPoolManager.SwapParams calldata params, BalanceDelta delta, bytes calldata data) returns (Currency feeCurrency, int128 hookDelta)` | Called from the initializer's `afterSwap` hook after every swap when `ON_SWAP_FLAG` is enabled. A positive `hookDelta` is settled by the initializer to the external hook in `feeCurrency` |
| `onGraduation(address asset, PoolKey calldata key, bytes calldata data)` | Called by `graduate()` when the pool reaches its graduation condition and `ON_GRADUATION_FLAG` is enabled |

## Hook Registration

External Doppler Hooks must be approved by the Airlock owner through `setDopplerHookState(address[] dopplerHooks, uint256[] flags)`.

The available flags are defined in `BaseDopplerHook.sol`:

- `ON_INITIALIZATION_FLAG`
- `ON_SWAP_FLAG`
- `ON_GRADUATION_FLAG`
- `REQUIRES_DYNAMIC_LP_FEE_FLAG`

Key behaviors:

- A pool initialized without a Doppler Hook can later attach one with `setDopplerHook()`
- A pool can opt out by setting the hook to `address(0)`
- A pool can swap from one approved hook to another while it is `Locked`
- `setDopplerHook()` can only be called by the asset timelock or its delegated authority
- `graduate()` only succeeds when a locked pool has a non-zero associated hook with the `ON_GRADUATION_FLAG` enabled

## Dynamic LP Fees

When a pool is initialized with a Doppler Hook, the pool itself is created as a dynamic-fee Uniswap v4 pool and the initializer seeds the initial LP fee with the `fee` field from `InitData`.

The external Doppler Hook associated with a locked pool can later update that LP fee directly through `updateDynamicLPFee()`, subject to the initializer's max LP fee cap.

That also means:

- A pool created without a Doppler Hook keeps the fixed fee from `InitData.fee`
- Adding a hook later with `setDopplerHook()` does not retroactively convert that pool into a dynamic-fee pool
- `onSwap()` itself does not override the pool LP fee on a per-swap basis; it only returns a hook fee delta back through the initializer's `afterSwap` hook
- Any custom fee logic implemented in an external Doppler Hook is distinct from the pool LP fee and is settled through the returned `hookDelta`, not by changing the Uniswap v4 LP fee automatically on each swap
13 changes: 8 additions & 5 deletions docs/DopplerHookMigrator.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

## Overview

The `DopplerHookMigrator` is a `LiquidityMigrator` module that migrates liquidity from an auction pool into a fresh Uniswap V4 pool. It also acts as a Uniswap V4 hook itself (`beforeInitialize`, `beforeSwap`, and `afterSwap`), allowing it to gate pool creation and forward swap events to an optional [Doppler Hook](./DopplerHook.md).
The `DopplerHookMigrator` is a `LiquidityMigrator` module that migrates liquidity from an auction pool into a fresh Uniswap V4 pool. It also acts as a Uniswap V4 hook itself (`beforeInitialize`, `beforeSwap`, and `afterSwap`), allowing it to gate pool creation and forward swap events to optional external Doppler hooks.

It integrates with the [`StreamableFeesLockerV2`](./StreamableFeesLockerV2.md) to lock the migrated liquidity for a configurable duration, and with the [`ProceedsSplitter`](./ProceedsSplitter.md) to optionally distribute a share of the proceeds to a designated recipient during migration.

Expand Down Expand Up @@ -43,20 +43,23 @@ See the contract source for details on how liquidity is computed and positions a

### Doppler Hook Support

The migrator supports pluggable [Doppler Hooks](./DopplerHook.md) that receive callbacks during the pool lifecycle. Doppler Hooks must be approved by the Airlock owner via `setDopplerHookState` before they can be used. Each hook is registered with a set of flags that determine which callbacks it supports (initialization, before swap, after swap, dynamic LP fee).
The migrator supports pluggable external Doppler hooks that receive callbacks during the pool lifecycle. Doppler hooks must be approved by the Airlock owner via `setDopplerHookState` before they can be used. Each hook is registered with a set of flags that determine which callbacks it supports (`ON_INITIALIZATION_FLAG`, `ON_BEFORE_SWAP_FLAG`, `ON_AFTER_SWAP_FLAG`, `REQUIRES_DYNAMIC_LP_FEE_FLAG`).

Key behaviors:

- A Doppler Hook can be set at initialization time or added/changed after migration via `setDopplerHook`
- A Doppler hook can be set at initialization time or added/changed later via `setDopplerHook`, but only once the pool is `Locked`
- A pool can opt out of its Doppler Hook by setting the address to `address(0)`
- Before every swap, the migrator's `beforeSwap` hook forwards the event to the associated Doppler Hook's `onBeforeSwap` callback (if the `ON_BEFORE_SWAP_FLAG` is enabled), allowing the hook to execute preparatory logic
- After every swap, the migrator's `afterSwap` hook forwards the event to the associated Doppler Hook's `onAfterSwap` callback (if the `ON_AFTER_SWAP_FLAG` is enabled), which can return a fee delta
- After every swap, the migrator's `afterSwap` hook forwards the event to the associated Doppler Hook's `onAfterSwap` callback (if the `ON_AFTER_SWAP_FLAG` is enabled), which can return a fee delta in the swap's unspecified token
- A positive post-swap hook delta is settled by the migrator to the external hook in the returned `feeCurrency`
- `setDopplerHook()` can only be called by the asset timelock or its delegated authority

### Dynamic LP Fees

Pools can be configured with either a fixed LP fee or a dynamic LP fee. When using dynamic fees:

- A Doppler Hook that requires dynamic fees (flag `REQUIRES_DYNAMIC_LP_FEE`) cannot be associated with a fixed-fee pool
- The associated Doppler hook must be registered with `REQUIRES_DYNAMIC_LP_FEE_FLAG`
- That requirement is checked during `initialize()`, re-checked during `migrate()`, and enforced again when replacing the hook on a locked pool
- The associated Doppler Hook can update the LP fee at any time via `updateDynamicLPFee`
- The maximum LP fee is capped at 15%

Expand Down
Loading