Skip to content

feat: add FeeDripper for smoothing protocol fee inflows to TokenJar#126

Open
mgretzke wants to merge 19 commits into
mainfrom
feat/FeeDripper
Open

feat: add FeeDripper for smoothing protocol fee inflows to TokenJar#126
mgretzke wants to merge 19 commits into
mainfrom
feat/FeeDripper

Conversation

@mgretzke

@mgretzke mgretzke commented Feb 20, 2026

Copy link
Copy Markdown
Collaborator

PR Description: FeeDripper

Summary

Introduce FeeDripper, a single-stream fee smoothing adapter that meters protocol fee inflows to TokenJar over a configurable window.

Motivation

Large fee inflows can overfill TokenJar and reduce capital efficiency around release mechanisms.
FeeDripper smooths inflows per currency over time while keeping operations permissionless and storage-efficient.

Key Design Choices

  • Single stream per currency (Drip state in one slot):
    • perBlockRate
    • endReleaseBlock
    • latestReleaseBlock
  • Two public entry points:
    • drip(currency): release accrued amount and (re)compute stream for remaining balance.
    • release(currency): release accrued amount only, without recomputing stream.
  • Configurable settings:
    • releaseWindow
    • windowResetBps (threshold for mid-window reset behavior)
  • Dust handling:
    • if remaining balance is below releaseWindow, flush immediately to avoid dust accumulation.

Notable Behavior

  • drip() is permissionless and can reset or keep a running window based on windowResetBps.
  • Mid-window small deposits below threshold keep the existing end block.
  • Equal/above-threshold deposits reset to a fresh releaseWindow.
  • For zero remaining balance after preparation, the contract writes a terminal zero-rate state and does not emit DripStarted.

Security / Risk Notes

  • Accepted tradeoff: permissionless delay griefing is still possible (attacker can slow flow by repeatedly calling drip with sufficient deposits), but funds cannot be stolen or redirected. The griefing slows the flow, which aligns with the metering goal. The real harm is reduced capital efficiency: fees sit idle in FeeDripper, and for volatile tokens the burn mechanism may miss favorable selling windows. The griefing can be made more expensive by increasing the windowResetBps threshold.
  • Unsupported token classes remain out of scope (fee-on-transfer/rebasing), consistent with existing protocol assumptions.

✨ Claude-Generated Content

Summary

Introduces FeeDripper, a single-stream fee smoothing adapter that meters protocol fee inflows to TokenJar over a configurable block window. Large fee inflows can overfill TokenJar and reduce capital efficiency around release mechanisms. FeeDripper smooths inflows per currency over time while keeping operations permissionless and storage-efficient.

Changes

New Contracts:

  • src/feeAdapters/FeeDripper.sol - Single-stream fee metering contract with configurable release window and basis point threshold for window resets
  • src/interfaces/IFeeDripper.sol - Interface with events (DripUpdated, Released, ReleaseSettingsSet), errors, and function signatures
    Tests:
  • test/FeeDripper.t.sol - Comprehensive unit tests covering constructor validation, release settings, drip/release mechanics, dust handling, griefing scenarios, currency independence, and gas benchmarks
    Gas Snapshots:
  • snapshots/FeeDripperTest.json - Gas snapshots for drip and release operations
    Other:
  • .github/workflows/claude-code-review.yml - Update Claude model version

Key Design

Feature Description
Single stream per currency Drip state packed in one slot: perBlockRate (uint160), endReleaseBlock (uint48), latestReleaseBlock (uint48)
Two entry points drip(currency) releases accrued + recomputes stream; release(currency) releases accrued only without recomputing
Configurable settings releaseWindow (default: 2000 blocks), windowResetBps (default: 50 bps threshold for mid-window reset)
Dust handling Balances below releaseWindow flush immediately to avoid dust accumulation
Overflow handling postDripBalance capped at uint160.max instead of reverting

Notes

  • Permissionless: Both drip() and release() are callable by anyone
  • Mid-window behavior: Deposits below windowResetBps threshold keep the existing end block; equal/above-threshold deposits reset to a fresh releaseWindow
  • Accepted tradeoff: Permissionless delay griefing is possible (attacker can slow flow by repeatedly calling drip with sufficient deposits), but funds cannot be stolen or redirected. Griefing can be made more expensive by increasing the windowResetBps threshold.
  • Not supported: Fee-on-transfer and rebasing tokens, consistent with existing protocol assumptions (v4 PoolManager, CCA auctions)

@mgretzke mgretzke requested a review from zhongeric February 20, 2026 17:03
@github-actions

github-actions Bot commented Feb 20, 2026

Copy link
Copy Markdown

🤖 Claude Code Review

🔄 Review in progress... Claude is analyzing this pull request. If a review can be seen below, it will be replaced by the results of this one.

⏱️ Reviews typically complete within 5-15 minutes depending on PR size.

Summary

This PR introduces a FeeDripper contract that smooths the flow of protocol fees into a TokenJar over a configurable block window. The implementation includes:

  • Linear drip mechanism with configurable release window and reset threshold
  • Gas-optimized storage packing via assembly
  • Dust accumulation prevention
  • Anti-griefing mechanisms via windowResetBps threshold

Critical Issue

Test compilation error: The test file references an event IFeeDripper.DripStarted (line 128) that doesn't exist in the interface. The interface only defines DripUpdated. This will cause the tests to fail to compile.

Code Quality

The contract is well-structured with:

  • Clean separation between drip() (incorporates new deposits) and release() (just releases accrued)
  • Proper handling of the "bricked currency" edge case identified in prior review (lines 92-95)
  • uint160.max cap instead of revert for overflow protection (line 59)
  • Comprehensive test coverage including griefing scenarios and edge cases

Feedback from Prior Review

Several items from the prior review appear to be addressed:

  • The bricked currency bug fix is in place
  • The uint160.max capping behavior was implemented
  • Assembly-based storage packing avoids memory allocation overhead

The test file needs to be fixed before this can merge.


💡 Want a fresh review? Add a comment containing @request-claude-review to trigger a new review at any time.

github-actions[bot]
github-actions Bot previously approved these changes Feb 20, 2026

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📋 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.

Comment thread src/feeAdapters/FeeDripper.sol
@github-actions github-actions Bot changed the title Feat/fee dripper feat: add FeeDripper for smoothing protocol fee inflows to TokenJar Feb 20, 2026
github-actions[bot]
github-actions Bot previously approved these changes Feb 20, 2026

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📋 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.

Comment thread src/feeAdapters/FeeDripper.sol Outdated
Comment thread src/feeAdapters/FeeDripper.sol Outdated
Comment thread src/feeAdapters/FeeDripper.sol Outdated
Comment thread src/feeAdapters/FeeDripper.sol
Comment thread src/feeAdapters/FeeDripper.sol
Comment thread src/interfaces/IFeeDripper.sol Outdated
Comment thread src/feeAdapters/FeeDripper.sol Outdated
Comment thread src/feeAdapters/FeeDripper.sol Outdated
Comment thread src/feeAdapters/FeeDripper.sol Outdated
Comment thread src/feeAdapters/FeeDripper.sol Outdated
Comment thread src/feeAdapters/FeeDripper.sol Outdated
/// Griefing note: an attacker can still delay flow by adding enough balance to meet the
/// reset threshold and repeatedly calling `drip()`, but cannot steal or redirect funds.
/// @param currency The currency to drip
function drip(Currency currency) external;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I dont really like the difference in name between drip and release

If a currency has a Drip set up for it, and some tokens are accrued already, then they are ready to "drip" out... so it feels wrong that release is the name of that function.

Much more boring but I feel like the functions should just be called like drip (for release), and dripAndReset or dripAndUpdate or something. But maybe thats too boring 😆

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am happy to change drip into something else, but on a first read, drip and dripAndUpdate feel harder to differentiate for me.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe a larger change but, what do you think of having drip() call release before updating the Drip state?

Would make it clearer when each should be used. I think it's possible but maybe missing something

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you feel like this is worth the additional sstore and sload just for clarity? I tried to already combine as much as possible within _prepareRelease and _releaseTokens.
I feel like we can expect integrators to differentiate between those two easily enough since the interface is so tiny with only three exposed functions in total.

@zhongeric zhongeric Feb 27, 2026

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Tbh I think it could be worth it for readability, but ultimately leave the decision up to you. To me, it seems much cleaner for these to be able to be called sequentially

Comment thread src/feeAdapters/FeeDripper.sol Outdated

// If the remaining balance is less than the release window, immediately release the remaining
// balance to skip dust accumulation
if (remainingBalance < _releaseSettings.releaseWindow) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This feels off to me? Why are we comparing a balance with a period of time?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is just to not have any uncollectible dust.
if remainingBalance < _releaseSettings.releaseWindow, the per block drip rate would be zero:
uint160 updatedPerBlockRate = uint160(remainingBalance / _releaseWindow);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This comparison (line 209) makes sense: if postDripBalance (token amount) is less than releaseWindow (number of blocks), the per-block rate would be 0, meaning dust that would never drip out. Flushing it immediately avoids dust accumulation.

Comment thread src/feeAdapters/FeeDripper.sol Outdated
Comment thread src/feeAdapters/FeeDripper.sol Outdated
Comment thread src/feeAdapters/FeeDripper.sol Outdated

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📋 Review verdict: COMMENT

👆 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. 2 inline comment(s) are attached below.

Comment thread src/feeAdapters/FeeDripper.sol Outdated
// Copy the drip state to memory
Drip memory dripState = drips[currency];

(, uint256 releasedAmount,) = _prepareRelease(currency, dripState);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This calls _prepareRelease which may trigger dust flush (adding remainingBalance to releasedAmount), but only latestReleaseBlock is updated here. If dust is flushed mid-stream, perBlockRate and endReleaseBlock remain stale, potentially causing underflow on the next call when currentBalance - releasedAmount is computed at line 139.

Consider either:

  1. Moving dust flush logic only to drip() as hensha256 suggested
  2. Or updating perBlockRate/endReleaseBlock when dust flush occurs in release()

Comment thread src/feeAdapters/FeeDripper.sol Outdated

// If the remaining balance is less than the release window, immediately release the remaining
// balance to skip dust accumulation
if (remainingBalance < _releaseSettings.releaseWindow) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This comparison (balance vs block count) works because it effectively checks if per-block rate would be < 1 token/block. A brief comment explaining this would address hensha256's concern about the seemingly mismatched types.

@Uniswap Uniswap deleted a comment from github-actions Bot Feb 23, 2026
github-actions[bot]
github-actions Bot previously approved these changes Feb 23, 2026

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📋 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.

@Uniswap Uniswap deleted a comment from github-actions Bot Feb 23, 2026
github-actions[bot]
github-actions Bot previously approved these changes Feb 23, 2026

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📋 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.

github-actions[bot]
github-actions Bot previously approved these changes Feb 23, 2026

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📋 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.

github-actions[bot]
github-actions Bot previously approved these changes Feb 23, 2026

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📋 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.

github-actions[bot]
github-actions Bot previously approved these changes Feb 23, 2026

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📋 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.

github-actions[bot]
github-actions Bot previously approved these changes Feb 23, 2026

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📋 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.

@mgretzke mgretzke requested a review from zhongeric February 24, 2026 10:30
@hensha256

Copy link
Copy Markdown

please can you push a new claude version to upgrade the CI flow to use opus 4.6?

Comment thread src/feeAdapters/FeeDripper.sol Outdated
returns (uint160 perBlockRate, uint48 endReleaseBlock, uint48 latestReleaseBlock)
{
// Read the drip state from storage and unpack into stack variables
assembly ("memory-safe") {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why does drip state now need to be in assembly? cant this just be a packed struct?

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

given the plan is for this contract to handle many millions of dollars i feel like saving a tiny bit of gas and sacrificing readability might not be worth it

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When using solidity structs directly, whether reading or writing, it will compile so that all data will be written into memory first, even if the struct is only declared from stack variables to be packed and stored in a single slot. Changing this saved around 500 gas just by skipping the unnecessary memory allocations.
It seemed to me like a very low hanging fruit (more so then the branchless logic) since its very isolated in its helper function and rather simple assembly code imo.

Comment thread src/feeAdapters/FeeDripper.sol Outdated
Comment thread src/feeAdapters/FeeDripper.sol Outdated
Comment thread src/feeAdapters/FeeDripper.sol Outdated
Comment thread src/feeAdapters/FeeDripper.sol
Comment thread src/feeAdapters/FeeDripper.sol Outdated
Comment thread src/feeAdapters/FeeDripper.sol Outdated
Comment thread src/feeAdapters/FeeDripper.sol Outdated
Comment thread src/feeAdapters/FeeDripper.sol
/// Griefing note: an attacker can still delay flow by adding enough balance to meet the
/// reset threshold and repeatedly calling `drip()`, but cannot steal or redirect funds.
/// @param currency The currency to drip
function drip(Currency currency) external;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe a larger change but, what do you think of having drip() call release before updating the Drip state?

Would make it clearer when each should be used. I think it's possible but maybe missing something

Comment thread src/interfaces/IFeeDripper.sol Outdated
Comment thread src/interfaces/IFeeDripper.sol
github-actions[bot]
github-actions Bot previously approved these changes Feb 26, 2026

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📋 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.

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📋 Review verdict: REQUEST_CHANGES

👆 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.

Comment thread test/FeeDripper.t.sol Outdated
uint48 expectedEnd = uint48(block.number + window);

vm.expectEmit(true, true, true, true);
emit IFeeDripper.DripStarted(Currency.unwrap(currency), expectedEnd, expectedRate);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This references IFeeDripper.DripStarted but the interface only defines DripUpdated. This will cause a compilation error.

Suggested change
emit IFeeDripper.DripStarted(Currency.unwrap(currency), expectedEnd, expectedRate);
emit IFeeDripper.DripUpdated(Currency.unwrap(currency), expectedEnd, expectedRate);

@github-actions

Copy link
Copy Markdown

🤖 Claude Code Review

Status: ⚠️ Partial (script error)
Job: View workflow run


The review was completed but there was an error posting inline comments.

Raw Review Output
No review output available

Please check the workflow logs for details.

@github-actions

Copy link
Copy Markdown

🤖 Claude Code Review

Status: ❌ Error
Job: View workflow run


⚠️ Review Failed

The automated code review encountered an error and could not complete.

Next Steps:

  1. Check the workflow logs for details
  2. Verify the GitHub Actions configuration
  3. Ensure the Anthropic API key is valid
  4. Check for any permission issues

If the problem persists, please contact your repository administrator.


You can manually re-trigger the review by pushing a new commit or re-running the workflow.

@github-actions

github-actions Bot commented Mar 2, 2026

Copy link
Copy Markdown

🤖 Claude Code Review

Status: ⚠️ Partial (script error)
Job: View workflow run


The review was completed but there was an error posting inline comments.

Raw Review Output
No review output available

Please check the workflow logs for details.

@github-actions

github-actions Bot commented Mar 2, 2026

Copy link
Copy Markdown

🤖 Claude Code Review

Status: ❌ Error
Job: View workflow run


⚠️ Review Failed

The automated code review encountered an error and could not complete.

Next Steps:

  1. Check the workflow logs for details
  2. Verify the GitHub Actions configuration
  3. Ensure the Anthropic API key is valid
  4. Check for any permission issues

If the problem persists, please contact your repository administrator.


You can manually re-trigger the review by pushing a new commit or re-running the workflow.

@github-actions

github-actions Bot commented Mar 2, 2026

Copy link
Copy Markdown

🤖 Claude Code Review

Status: ⚠️ Partial (script error)
Job: View workflow run


The review was completed but there was an error posting inline comments.

Raw Review Output
No review output available

Please check the workflow logs for details.

@github-actions

github-actions Bot commented Mar 2, 2026

Copy link
Copy Markdown

🤖 Claude Code Review

Status: ❌ Error
Job: View workflow run


⚠️ Review Failed

The automated code review encountered an error and could not complete.

Next Steps:

  1. Check the workflow logs for details
  2. Verify the GitHub Actions configuration
  3. Ensure the Anthropic API key is valid
  4. Check for any permission issues

If the problem persists, please contact your repository administrator.


You can manually re-trigger the review by pushing a new commit or re-running the workflow.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants