Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
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
54 changes: 47 additions & 7 deletions docs/RehypeDopplerHookInitializer.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ At a high level, `RehypeDopplerHook` adds a post-swap fee layer on top of a Dopp
- split collected fees across buybacks, beneficiary accounting, and LP reinvestment
- optionally split Rehype beneficiary fees among multiple pull-based recipients
- exempt the Bundler's atomic first buy from non-owner Rehype fees
- reserve an independently routed integrator share of each gross hook fee

Important: this fee schedule controls the Rehype hook fee collected in `onSwap`. It does not update the Uniswap v4 LP fee for the pool.

Expand All @@ -42,8 +43,9 @@ On `onInitialization`, the hook decodes `RehypeTypes.InitData` and stores:
| `durationSeconds` | Linear decay duration |
| `startingTime` | Fee schedule start time |
| `feeRoutingMode` | Whether buyback-designated fees are transferred immediately or routed into beneficiary accounting |
| `feeDistributionInfo` | Fee split matrix for asset-side and numeraire-side fees |
| `feeDistributionInfo` | Eight `uint64` WAD weights forming the asset-side and numeraire-side fee split config |
| `feeBeneficiaries` | Optional ordinary Rehype fee recipients and WAD shares over post-owner beneficiary accounting; an empty array preserves legacy `buybackDst` claims |
| `integratorConfig` | `IntegratorInitConfig` containing the integrator, immutable fee share, conversion ratios, and automatic payout setting |

The hook validates the configuration as follows:

Expand All @@ -56,6 +58,10 @@ The hook validates the configuration as follows:
- fee beneficiary addresses must be unique, non-zero, and sorted in ascending order
- each fee beneficiary share must be positive and all shares must sum to `WAD`
- the current Airlock owner need not appear; if included, it is an ordinary beneficiary and may have any positive share
- `integratorConfig.feeShare` must be at most `750_000` (75%)
- when `feeShare` is positive, `integrator` must be non-zero and each independent conversion ratio must be at most
`1_000_000_000`
- when `feeShare` is zero, `integrator` and both conversion ratios must also be zero

It also initializes a full-range LP position record for later reinvestment.

Expand All @@ -68,6 +74,8 @@ The hook stores a `FeeSchedule` per pool:
- once the full duration has elapsed, the fee stays at `endFee`
- for flat schedules (`startFee == endFee`), the fee never changes
- `lastFee` caches the last applied value and `FeeUpdated` is emitted only when the fee decreases
- `integratorFeeShare` is packed into the same storage slot but exposed separately through
`getIntegratorFeeShare(poolId)`

This makes the fee schedule lazy: it is evaluated when swaps happen, not by a background process.

Expand All @@ -82,25 +90,27 @@ For each ordinary external swap:
3. It computes the fee from the swap's unspecified token amount.
4. It self-collects that fee with `poolManager.take(...)`.
5. It returns the same positive `hookDelta` back to `DopplerHookInitializer`, which makes the swap accounting reflect the fee and settles the external hook's delta.
6. It reserves `floor(grossFee * 500 / 10_000)` in the separate Airlock owner bucket, regardless of whether `feeBeneficiaries` is empty.
7. It accumulates exactly `grossFee - ownerCut` into the per-pool balances used by normal routing.
6. It reserves `floor(grossFee * 500 / 10_000)` in the separate Airlock owner bucket.
7. Unless the atomic dev-buy exemption applies, it reserves
`floor(grossFee * integratorFeeShare / 1_000_000)` in a separate integrator bucket.
8. It sends the exact residual into the fee-distribution matrix, preserving conservation despite integer rounding.

### Atomic Dev Buy

`RehypeDopplerHookInitializer` stores an immutable authorized `bundler`. When `onInitialization` runs inside `Airlock.create`, the hook opens a transient, pool-specific exemption. The exemption can be consumed only by one swap whose PoolManager sender is that Bundler, and it expires at the end of the transaction.

For the exempt swap, the hook still computes the normal gross Rehype fee and reserves the usual 5% Airlock-owner cut. It collects and returns only that owner cut as the hook delta; the remaining non-owner Rehype fee is zero, so the dev buy does not add ordinary routing, beneficiary, buyback, or LP-reinvestment fees. Any later swap uses the ordinary fee path above.
For the exempt swap, the hook still computes the normal gross Rehype fee and reserves the usual 5% Airlock-owner share. It collects and returns only that owner share as the hook delta; both the integrator share and residual fee-distribution share are zero. The dev buy therefore does not add integrator, beneficiary, buyback, or LP-reinvestment fees. Any later swap uses the ordinary fee path above.

The exemption is available only during the atomic create-and-buy flow. Direct creators and ordinary swap routers cannot consume it. A reverted create or buy rolls back both pool creation and transient exemption state.

If both accumulated fee balances are still below `EPSILON`, the hook stops there and waits for more fees to build up.
For each currency, the hook compares the combined residual fee-distribution and pending integrator balance to `EPSILON`. It stops and waits for more fees only while both combined currency balances remain below the threshold.

Once enough fees have accumulated, the hook routes them according to `feeDistributionInfo`:

- asset fees can be sent directly as asset buyback, swapped into numeraire buyback, accrued as beneficiary fees, or allocated to LP reinvestment
- numeraire fees can be swapped into asset buyback, sent directly as numeraire buyback, accrued as beneficiary fees, or allocated to LP reinvestment

The routing matrix therefore operates only on the post-owner amount. When multiple beneficiaries are configured, their shares total `WAD` over the portion that ultimately reaches `beneficiaryFees0/1` after buyback routing, swaps, and LP handling—not over the gross hook fee or the entire post-owner remainder.
The routing matrix therefore operates only on the residual after the Airlock owner and integrator shares. When multiple beneficiaries are configured, their shares total `WAD` over the portion that ultimately reaches `beneficiaryFees0/1` after buyback routing, swaps, and LP handling—not over the gross hook fee or the entire residual.

## Fee Routing Modes

Expand All @@ -115,9 +125,27 @@ When `feeBeneficiaries` is empty, Rehype's `beneficiaryFees` are ultimately clai

Rehype fee beneficiaries are separate from the locked pool LP beneficiary shares managed by `DopplerHookInitializer`. A zero LP fee therefore produces no claimable LP fees even when the Rehype hook is charging and distributing its own fee.

## Integrator Fees

The integrator share is parallel to the Airlock-owner share and the residual fees sent to the fee-distribution config. It is not a beneficiary share and is unaffected by `feeDistributionInfo` or later `setFeeDistribution` calls. The configured fee share is immutable, the current integrator may atomically update both conversion ratios, enable or disable automatic payout, or rotate the integrator role.

`IntegratorInitConfig` contains:

- `integrator`: address controlling routing configuration and claims and receiving automatic payouts
- `feeShare`: immutable share of gross Rehype fees using a `1e6` denominator
- `assetFeesToNumeraireRatio`: ratio of asset-denominated integrator fees converted to numeraire using a `1e9` denominator
- `numeraireFeesToAssetRatio`: ratio of numeraire-denominated integrator fees converted to asset using a `1e9` denominator
- `automaticPayout`: whether processed fees are transferred automatically or accrued

Conversion ratios apply per source currency, not as value-based portfolio targets. The amount excluded from conversion stays in its source currency.

Integrator and residual fee-distribution inputs requesting the same swap direction are combined into one internal swap. Actual input consumed and output received are divided proportionally, with the rounding remainder going to the residual fee-distribution amount. If conversion simulation fails or consumes less than requested, the unconverted integrator input becomes claimable in its source currency and never enters the fee-distribution config.

Pending integrator fees have been collected but are not yet processed by a routing cycle. Claimable integrator fees have been processed and retained for manual claim. When automatic payout is disabled, all processed integrator balances become claimable. When it is enabled, the hook attempts one automatic transfer per output currency. A failed native or ERC-20 transfer becomes claimable instead of reverting the user's swap. Native automatic payouts use Solady's bounded `GAS_STIPEND_NO_GRIEF`.

## Updating Fee Distribution

The stored `buybackDst` can call `setFeeDistribution(poolId, ...)` to replace all eight weights in the pool's fee distribution matrix. The caller must exactly match `getPoolInfo(poolId).buybackDst`, and both the asset-fee row and the numeraire-fee row must each sum to `WAD`.
The stored `buybackDst` can call `setFeeDistribution(poolId, ...)` to replace all eight weights in the pool's fee distribution config. The external setter and getter retain their original `uint256` ABI while the values are stored as packed `uint64` WAD weights. The caller must exactly match `getPoolInfo(poolId).buybackDst`, and both the asset-fee row and the numeraire-fee row must each sum to `WAD`.

This authority applies whether `feeBeneficiaries` is empty or configured. With configured beneficiaries, `buybackDst` controls the routing matrix but does not receive beneficiary fees unless it is also included as a beneficiary. Updating the matrix does not change `feeRoutingMode`, the fee schedule, or beneficiary shares.

Expand Down Expand Up @@ -149,6 +177,14 @@ For pools with configured fee beneficiaries:

The role-based owner cut is separate in both modes. Only the current `airlock.owner()` can call `claimAirlockOwnerFees(asset)`, and it receives the entire unclaimed owner bucket even if some fees accrued before an ownership transfer. The former owner loses access to that role bucket. If an owner is also listed as an ordinary beneficiary, that ordinary share continues to use `collectFees` and `updateBeneficiary`; it remains attached to the listed address and does not migrate when Airlock ownership changes.

Integrator fees use a separate claim path:

- only the current configured integrator may call `claimIntegratorFees(asset, to)`
- `to` may differ from the integrator but must be non-zero
- a claim transfers all claimable currency0 and currency1 balances
- balances are cleared before transfer, and claim transfers revert on failure
- rotating the integrator moves control of existing claimable balances to the new address

All claim functions are `nonReentrant`.

This feature is specific to `RehypeDopplerHookInitializer`. Rehype migrator initialization data and claim behavior are unchanged.
Expand All @@ -161,7 +197,11 @@ The main per-pool views are:
- `getFeeDistributionInfo(poolId)`
- `getFeeRoutingMode(poolId)`
- `getFeeSchedule(poolId)`
- `getIntegratorFeeShare(poolId)`
- `getHookFees(poolId)`
- `getIntegratorRoutingConfig(poolId)`
- `getPendingIntegratorFees(poolId)`
- `getClaimableIntegratorFees(poolId)`
- `getPosition(poolId)`
- `getPoolKey(poolId)`
- `getShares(poolId, beneficiary)`
Expand Down
4 changes: 2 additions & 2 deletions docs/RehypeDopplerHookMigrator.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ On `onInitialization`, the hook decodes `RehypeTypes.MigratorInitData` and store
| `buybackDst` | Recipient for direct buybacks and claimed beneficiary fees |
| `customFee` | Static hook fee, in millionths |
| `feeRoutingMode` | Whether buyback-designated fees are transferred immediately or routed into beneficiary accounting |
| `feeDistributionInfo` | Fee split matrix for asset-side and numeraire-side fees |
| `feeDistributionInfo` | Eight `uint64` WAD weights forming the asset-side and numeraire-side fee split config |

The hook validates that each row of `feeDistributionInfo` sums to `WAD`, stores the static fee in `getHookFees(poolId).customFee`, and initializes a full-range LP position record for later reinvestment.

Expand Down Expand Up @@ -85,7 +85,7 @@ The hook exposes three public management paths:

- `collectFees(asset)`: transfers accumulated `beneficiaryFees0/1` to `buybackDst`
- `claimAirlockOwnerFees(asset)`: transfers accumulated `airlockOwnerFees0/1` to the current Airlock owner
- `setFeeDistribution(poolId, ...)`: lets `buybackDst` update the fee split matrix for that pool
- `setFeeDistribution(poolId, ...)`: lets `buybackDst` update the eight WAD weights through the original `uint256` ABI while storing them as packed `uint64` values

`customFee` itself is fixed at initialization time and is not updated by this contract.

Expand Down
16 changes: 14 additions & 2 deletions script/TestDeployment.s.sol
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,12 @@ import { NoOpMigrator } from "src/migrators/NoOpMigrator.sol";
import { DopplerERC20V1, VestingSchedule } from "src/tokens/DopplerERC20V1.sol";
import { DopplerERC20V1Factory } from "src/tokens/DopplerERC20V1Factory.sol";
import { BeneficiaryData } from "src/types/BeneficiaryData.sol";
import { FeeDistributionInfo, FeeRoutingMode, InitData as RehypeInitData } from "src/types/RehypeTypes.sol";
import {
FeeDistributionInfo,
FeeRoutingMode,
InitData as RehypeInitData,
IntegratorInitConfig
} from "src/types/RehypeTypes.sol";
import { WAD } from "src/types/Wad.sol";

/// @notice Thrown when this simulation-only script is invoked in a broadcast context.
Expand Down Expand Up @@ -281,7 +286,14 @@ abstract contract TestDeployment is DeployBase {
startingTime: 0,
feeRoutingMode: FeeRoutingMode.RouteToBeneficiaryFees,
feeDistributionInfo: distribution,
feeBeneficiaries: _feeBeneficiaries(deployments.deployer, deployments.treasury)
feeBeneficiaries: _feeBeneficiaries(deployments.deployer, deployments.treasury),
integratorConfig: IntegratorInitConfig({
integrator: address(0),
feeShare: 0,
assetFeesToNumeraireRatio: 0,
numeraireFeesToAssetRatio: 0,
automaticPayout: false
})
});

InitData memory initData = InitData({
Expand Down
4 changes: 2 additions & 2 deletions script/utils/Versions.sol
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,8 @@ contract Versions {
uint8 public constant UNISWAP_V2_MIGRATOR_SPLIT_VERSION = 0;
uint8 public constant NO_OP_MIGRATOR_VERSION = 0;
// --- Doppler Hooks ---
uint8 public constant REHYPE_DOPPLER_HOOK_INITIALIZER_VERSION = 4;
uint8 public constant REHYPE_DOPPLER_HOOK_MIGRATOR_VERSION = 2;
uint8 public constant REHYPE_DOPPLER_HOOK_INITIALIZER_VERSION = 5;
uint8 public constant REHYPE_DOPPLER_HOOK_MIGRATOR_VERSION = 3;
uint8 public constant SWAP_RESTRICTOR_DOPPLER_HOOK_VERSION = 0;
// --- Other ---
uint8 public constant DOPPLER_LENS_QUOTER_VERSION = 0;
Expand Down
Loading