feat: collect market fees through the Doppler initializer - #1
Merged
Conversation
Doppler holds a pool's trading fees in its initializer and distributes
them by share. `collectFees(PoolId)` harvests the outstanding amount and
releases only the caller's portion, to a beneficiary registered when the
pool was created. Uniswap V4 is a singleton, so a pool is a `PoolKey`
rather than a contract, and its id is the hash of that key.
`IDopplerHookInitializer` declares that surface. Each signature was
checked against `DopplerHookInitializer` and `RehypeDopplerHookInitializer`
on Base Mainnet, and the seven-element `getState` tuple was decoded
against the live contract:
getState(address) 0x1bab58f5
getShares(bytes32,address) 0x5ebb58fb
collectFees(bytes32) 0x817db73b
`RIKRoyaltySplitter.collectPoolFees` now takes the asset, reads the pool
key from the initializer recorded at launch, derives the pool id, and
credits the measured balance delta. Taking the initializer from storage
rather than from the caller keeps collection from being aimed elsewhere.
Beneficiaries are fixed when a pool is created and cannot be added
afterwards, so `RIKLauncher` reads `getShares(poolId, splitter)` after
`create` and reverts with `SplitterNotBeneficiary` when it is zero. It
also rejects a native numeraire, since releases are ERC20 transfers and
the splitter cannot hold native value.
The mock reproduces `FeesManager` rather than approximating it —
cumulative fees, per-beneficiary high-water marks, release to the caller
— so the tests exercise the semantics Doppler actually has. `MockAirlock`
delegates to the initializer the way the real Airlock delegates to
`poolInitializer.initialize`. `MarketSystem.invariant.t.sol` absorbs the
two invariants the splitter-only campaign held, which is removed.
The README documents the calling convention: the `InitData` layout, the
beneficiary rules `storeBeneficiaries` enforces (ascending, summing to
WAD, `airlock.owner()` with at least 5%), and the note that
`Airlock.create` returns the asset in its `pool` slot for a V4
initializer.
286 tests pass, slither reports zero findings, lint and fmt are clean,
and the deep soak runs 262,144 calls per system invariant.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Doppler holds a pool's trading fees in its initializer and distributes them by share.
collectFees(PoolId)harvests the outstanding amount and releases only the caller's portion, to a beneficiary registered when the pool was created. Uniswap V4 is a singleton, so a pool is aPoolKeyrather than a contract, and its id is the hash of that key.IDopplerHookInitializerdeclares that surface. Each signature was checked againstDopplerHookInitializerandRehypeDopplerHookInitializeron Base Mainnet, and the seven-elementgetStatetuple was decoded against the live contract:RIKRoyaltySplitter.collectPoolFeesnow takes the asset, reads the pool key from the initializer recorded at launch, derives the pool id, and credits the measured balance delta. Taking the initializer from storage rather than from the caller keeps collection from being aimed elsewhere.Beneficiaries are fixed when a pool is created and cannot be added afterwards, so
RIKLauncherreadsgetShares(poolId, splitter)aftercreateand reverts withSplitterNotBeneficiarywhen it is zero. It also rejects a native numeraire, since releases are ERC20 transfers and the splitter cannot hold native value.The mock reproduces
FeesManagerrather than approximating it — cumulative fees, per-beneficiary high-water marks, release to the caller — so the tests exercise the semantics Doppler actually has.MockAirlockdelegates to the initializer the way the real Airlock delegates topoolInitializer.initialize.MarketSystem.invariant.t.solabsorbs the two invariants the splitter-only campaign held, which is removed.The README documents the calling convention: the
InitDatalayout, the beneficiary rulesstoreBeneficiariesenforces (ascending, summing to WAD,airlock.owner()with at least 5%), and the note thatAirlock.createreturns the asset in itspoolslot for a V4 initializer.286 tests pass, slither reports zero findings, lint and fmt are clean, and the deep soak runs 262,144 calls per system invariant.