Skip to content

[BOOST-6102] Streaming Incentives Campaign Creation & Configuration #511

Merged
mmackz merged 19 commits intofeature/streaming-incentivesfrom
matthew/boost-6102-epic-1-campaign-creation-configuration
Jan 29, 2026
Merged

[BOOST-6102] Streaming Incentives Campaign Creation & Configuration #511
mmackz merged 19 commits intofeature/streaming-incentivesfrom
matthew/boost-6102-epic-1-campaign-creation-configuration

Conversation

@mmackz
Copy link
Contributor

@mmackz mmackz commented Jan 29, 2026

Summary

Implements Epic 1 of Streaming Incentives - the foundational contracts for campaign creation and configuration.

Linear Tickets:

  • closes BOOST-6102: Epic 1: Campaign Creation & Configuration ✅
  • closes BOOST-6110: 1.1 Create Streaming Campaign ✅
  • closes BOOST-6111: 1.2 Initialize Streaming Campaign ✅
  • closes BOOST-6112: 1.3 Configure Protocol Fee ✅
  • closes BOOST-6113: 1.4 Configure Fee Receiver ✅

Contracts Added

  • StreamingManager.sol - Factory contract that deploys campaigns, handles protocol fees, maintains campaign registry. UUPS upgradeable.
  • StreamingCampaign.sol - Per-campaign contract (clone) that holds reward tokens and will process merkle claims

Features

  • Story 1.1: createCampaign() - Deploys campaign clone, disburses funds from budget, handles fee calculation
  • Story 1.2: initialize() - Sets up campaign state (manager, budget, creator, config hash, reward token, timing)
  • Story 1.3: setProtocolFee() - Owner-only setter with validation and event
  • Story 1.4: setProtocolFeeReceiver() - Owner-only setter with validation and event

UUPS Upgradeability (StreamingManager)

  • Inherits Solady Initializable, UUPSUpgradeable, Ownable
  • Constructor disables initializers (prevents implementation from being initialized)
  • initialize(owner, campaignImpl, protocolFee, protocolFeeReceiver) with initializer modifier
  • _authorizeUpgrade(address) with onlyOwner for upgrade authorization
  • setCampaignImplementation(address) - allows updating campaign template without redeploying manager
  • Storage gap (__padding + __gap[50]) for future upgrade safety
  • Indexed event parameters for better querying
  • Follows same pattern as BoostCore (core upgradeable, clones non-upgradeable)

Test Coverage

  • 43 tests passing
  • Initialize validation (8 tests) - includes double-init prevention, implementation direct init prevention
  • Campaign creation success cases including edge cases like 0% and 100% fee (5 tests)
  • Campaign creation revert cases (7 tests)
  • Campaign initialization (5 tests)
  • Protocol fee management (4 tests)
  • Protocol fee receiver management (3 tests)
  • Campaign implementation management (4 tests)
  • UUPS upgrade tests (4 tests) - upgrade success, owner-only, state preservation, proxiableUUID

Design Decisions

  • Standalone from BoostCore (not AIncentive subclass)
  • Uses ManagedBudget for funding (existing budget system)
  • Minimal clone pattern for gas-efficient campaign deployment
  • Scaffolding for future merkle claims (merkleRoot, claimed mapping, onlyStreamingManager modifier)
  • Campaign clones stay non-upgradeable (like BoostCore pattern)

Test plan

  • All 43 unit tests pass (forge test --match-contract StreamingManagerTest)
  • Initialize validation tests (proxy pattern)
  • Fee calculation tests (0%, 10%, 100%)
  • Authorization tests (budget permissions, owner-only setters)
  • Edge case tests (max fee, zero fee, insufficient funds)
  • UUPS upgrade tests (state preservation, access control)

Generated with Claude Code

@changeset-bot
Copy link

changeset-bot bot commented Jan 29, 2026

⚠️ No Changeset found

Latest commit: eddd95f

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

Click here to learn what changesets are, and how to add one.

Click here if you're a maintainer who wants to add a changeset to this PR

💥 An error occurred when fetching the changed packages and changesets in this PR
Some errors occurred when validating the changesets config:
The package or glob expression "@boostxyz/test" is specified in the `ignore` option but it is not found in the project. You may have misspelled the package name or provided an invalid glob expression. Note that glob expressions must be defined according to https://www.npmjs.com/package/micromatch.

@github-actions github-actions bot added the EVM label Jan 29, 2026
@jonathandiep
Copy link
Contributor

Fails
🚫

Your PR body must reference a Github issue using a valid keyword, or your PR title must include the internal Boost ticket number, ie: [BOOST-1234] Innovate like nuts

Warnings
⚠️

Are you sure you want to be submitting a change without including a changeset? If you're just changing docs or tests, you probably don't need to. See the publishing section of the README for more info.

Generated by 🚫 dangerJS against a23739b

1 similar comment
@jonathandiep
Copy link
Contributor

Fails
🚫

Your PR body must reference a Github issue using a valid keyword, or your PR title must include the internal Boost ticket number, ie: [BOOST-1234] Innovate like nuts

Warnings
⚠️

Are you sure you want to be submitting a change without including a changeset? If you're just changing docs or tests, you probably don't need to. See the publishing section of the README for more info.

Generated by 🚫 dangerJS against a23739b

@mmackz mmackz changed the title Epic 1: Campaign Creation & Configuration [BOOST-6102] Streaming Incentives Campaign Creation & Configuration Jan 29, 2026
@jonathandiep
Copy link
Contributor

Fails
🚫

Your PR body must reference a Github issue using a valid keyword, or your PR title must include the internal Boost ticket number, ie: [BOOST-1234] Innovate like nuts

Warnings
⚠️

Are you sure you want to be submitting a change without including a changeset? If you're just changing docs or tests, you probably don't need to. See the publishing section of the README for more info.

Generated by 🚫 dangerJS against a23739b

@jonathandiep
Copy link
Contributor

Warnings
⚠️

Are you sure you want to be submitting a change without including a changeset? If you're just changing docs or tests, you probably don't need to. See the publishing section of the README for more info.

Generated by 🚫 dangerJS against a23739b

4 similar comments
@jonathandiep
Copy link
Contributor

Warnings
⚠️

Are you sure you want to be submitting a change without including a changeset? If you're just changing docs or tests, you probably don't need to. See the publishing section of the README for more info.

Generated by 🚫 dangerJS against a23739b

@jonathandiep
Copy link
Contributor

Warnings
⚠️

Are you sure you want to be submitting a change without including a changeset? If you're just changing docs or tests, you probably don't need to. See the publishing section of the README for more info.

Generated by 🚫 dangerJS against a23739b

@jonathandiep
Copy link
Contributor

Warnings
⚠️

Are you sure you want to be submitting a change without including a changeset? If you're just changing docs or tests, you probably don't need to. See the publishing section of the README for more info.

Generated by 🚫 dangerJS against a23739b

@jonathandiep
Copy link
Contributor

Warnings
⚠️

Are you sure you want to be submitting a change without including a changeset? If you're just changing docs or tests, you probably don't need to. See the publishing section of the README for more info.

Generated by 🚫 dangerJS against a23739b

@mmackz mmackz changed the title [BOOST-6102] Streaming Incentives Campaign Creation & Configuration Epic 1: Campaign Creation & Configuration Jan 29, 2026
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In `@packages/evm/contracts/streaming/StreamingManager.sol`:
- Around line 121-164: The createCampaign() function performs external calls
(LibClone.clone -> budget.disburse() and StreamingCampaign.initialize()) before
updating state (campaignCount and campaigns), so add a reentrancy protection:
either apply a nonReentrant modifier to createCampaign() or reorder to follow
checks-effects-interactions (increment campaignCount and set
campaigns[campaignId] before making external calls), ensuring you reference the
same symbols (createCampaign, campaignCount, campaigns, budget.disburse,
StreamingCampaign.initialize, LibClone.clone) when implementing the change.
🧹 Nitpick comments (4)
packages/evm/contracts/streaming/StreamingManager.sol (1)

6-6: Unused import: SafeTransferLib.

SafeTransferLib is imported but not used in this contract. The contract relies on ABudget.disburse() for all token transfers.

🧹 Proposed fix
-import {SafeTransferLib} from "@solady/utils/SafeTransferLib.sol";

Also remove the using declaration at line 15:

-    using SafeTransferLib for address;
packages/evm/test/streaming/StreamingManager.t.sol (2)

162-163: Clarify vm.expectEmit usage for unpredictable addresses.

Using vm.expectEmit(true, true, true, false) correctly skips non-indexed data verification since the campaign address is unknown. However, the subsequent emit statement still provides all values including address(0) for the campaign address, which could be misleading to readers.

Consider adding a comment clarifying that the non-indexed data check is disabled, so the placeholder values are ignored.


294-308: Test relies on specific internal budget behavior.

The test comment explains that with 1000 ether requested, the fee (100 ether) succeeds first, depleting the budget, and then the net transfer (900 ether) fails. This assumes a specific execution order and partial success behavior in ABudget.disburse. If the budget implementation changes to check total availability upfront, this test may behave differently.

Consider adding an explicit comment or a more isolated test that doesn't depend on partial disburse success.

packages/evm/contracts/streaming/StreamingCampaign.sol (1)

100-104: onlyStreamingManager modifier is defined but currently unused.

This modifier is scaffolding for future functions (e.g., setMerkleRoot, claim). Consider adding a comment indicating its intended future use, or defer adding it until the functions that require it are implemented to avoid dead code.

📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Disabled knowledge base sources:

  • Linear integration is disabled

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 3774ffc and a23739b.

📒 Files selected for processing (3)
  • packages/evm/contracts/streaming/StreamingCampaign.sol
  • packages/evm/contracts/streaming/StreamingManager.sol
  • packages/evm/test/streaming/StreamingManager.t.sol
🧰 Additional context used
🧠 Learnings (1)
📓 Common learnings
Learnt from: mmackz
Repo: boostxyz/boost-protocol PR: 479
File: packages/sdk/src/Incentives/ERC20VariableCriteriaIncentiveV2.ts:285-292
Timestamp: 2025-09-22T23:21:17.136Z
Learning: In the boost-protocol repository, security improvements like targetContract filtering should be kept separate from feature-focused PRs and handled in dedicated security improvement PRs.
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: Cursor Bugbot
  • GitHub Check: Verify / Verify
🔇 Additional comments (12)
packages/evm/contracts/streaming/StreamingManager.sol (4)

114-114: Consider allowing startTime == block.timestamp for immediate campaign starts.

The current check startTime < block.timestamp allows startTime == block.timestamp, which is correct for same-block campaign creation. However, this edge case isn't explicitly tested.


100-176: LGTM on core createCampaign logic.

The validation sequence is comprehensive: authorization check, parameter validation, fee calculation, disburse operations with failure checks, and proper initialization. The use of pre-increment for campaignId ensures IDs start at 1, which is a good practice for distinguishing unset values.


187-192: LGTM on setProtocolFee.

Proper owner-only access control, bounds validation, and event emission with old/new values for auditability.


196-201: LGTM on setProtocolFeeReceiver.

Proper owner-only access control, zero-address validation, and event emission.

packages/evm/test/streaming/StreamingManager.t.sol (4)

25-54: LGTM on test setup.

The setup properly initializes all components with appropriate authorization roles. Good practice using constants for addresses and fee values.


196-236: Good edge case coverage for 100% fee scenario.

This test validates an important edge case where netAmount = 0 and the campaign is initialized with zero rewards. The assertions verify both token balances and campaign state correctly.


120-152: Good zero-fee edge case coverage.

Properly tests that no fee is taken when protocolFee = 0 and the campaign receives the full amount.


365-392: LGTM on event verification test.

Good use of vm.recordLogs() and manual log parsing to verify CampaignInitialized event emission with correct indexed parameters.

packages/evm/contracts/streaming/StreamingCampaign.sol (4)

56-58: LGTM on implementation protection.

Correctly uses _disableInitializers() in the constructor to prevent the implementation contract from being initialized directly.


69-98: Initialize is unguarded but safe in current usage pattern.

The initialize() function lacks explicit access control (e.g., onlyStreamingManager), relying instead on the initializer modifier to prevent re-initialization. This is safe because StreamingManager.createCampaign() atomically clones and initializes in the same transaction, leaving no window for front-running.

However, for defense-in-depth and to make the intended usage pattern explicit, consider adding the onlyStreamingManager check. This would require the manager address to be passed as an immutable clone argument or checked via msg.sender == tx.origin-style patterns, which adds complexity.

Given the current atomic pattern is safe and the onlyStreamingManager modifier exists for future claim functions, the current implementation is acceptable.


34-38: Scaffolding for merkle claims noted.

The merkleRoot and claimed mapping are defined but have no setters or claim logic. Per the PR objectives, this is intentional scaffolding for future merkle claim processing in subsequent epics.


1-105: Overall: Well-structured cloneable campaign contract.

The contract follows the minimal proxy pattern correctly with proper use of Solady's Initializable. State variables are appropriately typed (using uint64 for timestamps to save storage), and the event provides good observability for off-chain indexing.

✏️ Tip: You can disable this entire section by setting review_details to false in your review settings.

@mmackz mmackz changed the title Epic 1: Campaign Creation & Configuration [BOOST-6102] Streaming Incentives Campaign Creation & Configuration Jan 29, 2026
@jonathandiep
Copy link
Contributor

Warnings
⚠️

Are you sure you want to be submitting a change without including a changeset? If you're just changing docs or tests, you probably don't need to. See the publishing section of the README for more info.

Generated by 🚫 dangerJS against a23739b

2 similar comments
@jonathandiep
Copy link
Contributor

Warnings
⚠️

Are you sure you want to be submitting a change without including a changeset? If you're just changing docs or tests, you probably don't need to. See the publishing section of the README for more info.

Generated by 🚫 dangerJS against a23739b

@jonathandiep
Copy link
Contributor

Warnings
⚠️

Are you sure you want to be submitting a change without including a changeset? If you're just changing docs or tests, you probably don't need to. See the publishing section of the README for more info.

Generated by 🚫 dangerJS against a23739b

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Approval

I've completed the review and confirm that all issues have been addressed:

✅ Previously Flagged Issue - RESOLVED

The reentrancy vulnerability in createCampaign() has been properly fixed. State updates (campaignCount and campaigns mapping) now occur before external calls to budget.disburse() and StreamingCampaign.initialize(), following the checks-effects-interactions pattern.

✅ Code Review Summary

StreamingManager.sol:

  • Proper input validation (zero address checks, fee limits, timing validation)
  • State management follows best practices
  • Clean event emission
  • Owner-only setters properly protected

StreamingCampaign.sol:

  • Correct use of Initializable pattern from Solady
  • Implementation contract properly disabled via constructor
  • Access control scaffolding in place for future functionality
  • Clean state management

Tests:

  • 30 tests passing with comprehensive coverage
  • Edge cases handled (0% fee, 100% fee, authorization, validation)

No security concerns or code quality issues found. The PR is ready to merge! 🚀

@boostxyz boostxyz deleted a comment from jonathandiep Jan 29, 2026
@boostxyz boostxyz deleted a comment from jonathandiep Jan 29, 2026
@boostxyz boostxyz deleted a comment from jonathandiep Jan 29, 2026
@boostxyz boostxyz deleted a comment from jonathandiep Jan 29, 2026
@boostxyz boostxyz deleted a comment from jonathandiep Jan 29, 2026
@boostxyz boostxyz deleted a comment from coderabbitai bot Jan 29, 2026
@jonathandiep
Copy link
Contributor

Warnings
⚠️

Are you sure you want to be submitting a change without including a changeset? If you're just changing docs or tests, you probably don't need to. See the publishing section of the README for more info.

Generated by 🚫 dangerJS against eddd95f

2 similar comments
@jonathandiep
Copy link
Contributor

Warnings
⚠️

Are you sure you want to be submitting a change without including a changeset? If you're just changing docs or tests, you probably don't need to. See the publishing section of the README for more info.

Generated by 🚫 dangerJS against eddd95f

@jonathandiep
Copy link
Contributor

Warnings
⚠️

Are you sure you want to be submitting a change without including a changeset? If you're just changing docs or tests, you probably don't need to. See the publishing section of the README for more info.

Generated by 🚫 dangerJS against eddd95f

@boostxyz boostxyz deleted a comment from coderabbitai bot Jan 29, 2026
@boostxyz boostxyz deleted a comment from jonathandiep Jan 29, 2026
@jonathandiep
Copy link
Contributor

Warnings
⚠️

Are you sure you want to be submitting a change without including a changeset? If you're just changing docs or tests, you probably don't need to. See the publishing section of the README for more info.

Generated by 🚫 dangerJS against eddd95f

@mmackz mmackz merged commit 4d2a8fa into feature/streaming-incentives Jan 29, 2026
10 checks passed
@mmackz mmackz deleted the matthew/boost-6102-epic-1-campaign-creation-configuration branch January 29, 2026 04:48
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants