Conversation
🤖 Claude Code Review
Review SummaryThis PR introduces The design is sound and the tests are thorough. The linked-list management for keg cleanup is well-implemented, handling edge cases like middle-keg deletion and tail updates correctly. Key ObservationsPotential issue with In
However, looking at the flow: After more thought:
These should stay in sync, so this is fine. The subtraction at line 72 is safe because new deposits can only increase the actual balance above the tracked balance.
No access control on Minor items
Overall this is clean, well-tested code with good gas optimization via deferred BPS division. 💡 Want a fresh review? Add a comment containing |
There was a problem hiding this comment.
📋 Review verdict: APPROVE
👆 The main review comment above is the source of truth for this PR review. It is automatically updated on each review cycle, so always refer to it for the most current feedback.
This formal review submission is for the verdict only. 1 inline comment(s) are attached below.
| uint32 prev = 0; | ||
| uint32 curr = $tap.head; | ||
| uint256 blockNumber = block.number; | ||
| // Itereate through all kegs. This can be very costly if there are lot of kegs. |
There was a problem hiding this comment.
Typo: "Itereate" should be "Iterate".
| // Itereate through all kegs. This can be very costly if there are lot of kegs. | |
| // Iterate through all kegs. This can be very costly if there are lot of kegs. |
| /// @param id The id of the keg to release | ||
| function _release(Keg memory keg, uint32 id) internal returns (uint192 releasedAmount) { | ||
| uint48 minBlock = uint48(_min(block.number, keg.endBlock)); | ||
| releasedAmount = uint192(keg.perBlockReleaseAmount * (minBlock - keg.lastReleaseBlock)); |
There was a problem hiding this comment.
why arent minBlock and releasedAmount just uint256s? I think smaller types are more expensive because the compiler has to clean upper bits
There was a problem hiding this comment.
oh i see why, its packing in Keg struct
| /// @notice Mapping of currencies to Taps | ||
| mapping(Currency => Tap) private $_taps; | ||
| /// @notice Linked list of kegs. Taps manage the head and tail of their respective kegs. | ||
| mapping(uint32 => Keg) private $_kegs; |
There was a problem hiding this comment.
At a high level - whats the motivation to have multiple kegs per currency? If the contract has say $1m USDC in it, dont we want to limit the total USDC that can get dripped in each block? That to me feels like 1 tap that just gets topped up over time with each deposit and no separation of different release schedules for different sets of tokens. But maybe theres a specific reason for that?
| Currency currency; /// @notice The currency associated with the keg | ||
| uint48 endBlock; /// @notice The block at which the deposit will be fully released | ||
| uint48 lastReleaseBlock; /// @notice The block at which the last release was made | ||
| uint192 perBlockReleaseAmount; /// @notice The absolute amount of the currency released per block |
There was a problem hiding this comment.
this comment isnt right - its confusingly still multiplied by BPS
|
|
||
| /// @notice Transfers the released amount to the token jar | ||
| function _process(Currency _currency, uint192 _releasedAmount) internal returns (uint192) { | ||
| // Because we deferred dividing by BPS when storing the perBlockReleaseAmount, we need to divide |
There was a problem hiding this comment.
whats the reason for this? it feels potentially bug-prone to leave it multiplied by BPS unless comments and naming are both clearer about that in the rest of the contract
The recipient is intended to be
FeeTapper, a singleton contract which holds a multitude of active deposits. Each currency has a uniqueTap, which tracks its balance over time. Each call tosynccreates a new deposit, also calledKeg. EachKegholds a per block release amount and an end block. Kegs can be released in batch (all at once per currency, or one by one). Kegs are stored in a flat linked list in storage and Taps hold pointers to the head/tail within the list.FeeTapperstreams fees over time, ensuring that large fee payments are incrementally sent to the fee recipient. For more details on the token jar implementation, check out the https://github.com/Uniswap/protocol-fees/blob/main/src/releasers/ExchangeReleaser.sol✨ Claude-Generated Content
Summary
Adds the FeeTapper contract, a singleton that handles streaming protocol fees to TokenJar using a configurable per-block release rate to smooth fee distribution.
Changes
src/feeAdapters/FeeTapper.sol- Singleton contract managing fee streaming with linked-list "kegs" for each currency "tap"src/interfaces/IFeeTapper.sol- Interface defining Tap/Keg structs and contract methodstest/FeeTapper.t.sol- Comprehensive test suite (573 lines) covering sync, release, and edge casessnapshots/FeeTapperTest.json- Gas benchmarks for operationsKey Features
perBlockReleaseRate(default 10 bps/block = 1000 blocks for full release)