Skip to content

feat: streaming pricing strategy#8

Open
tnrdd wants to merge 19 commits into
devfrom
feat/streaming-strat
Open

feat: streaming pricing strategy#8
tnrdd wants to merge 19 commits into
devfrom
feat/streaming-strat

Conversation

@tnrdd

@tnrdd tnrdd commented Jun 19, 2026

Copy link
Copy Markdown
Collaborator

Superfluid streaming pricing strategy for Markee. Backers stream ETHx into a SuperApp leaderboard ranked by stream rate; existing v1.3 lump-sum boards migrate onto it in place.

  • StreamingLeaderboard SuperApp + factory
  • Top markee streams 62% to the beneficiary, losers refunded ~net-zero, 38% to per-funder RevNet settlement
  • Batchable single-tx open, jail-safe callbacks, v1.3-compat reads
  • Integration tests on a Base fork

tnrdd added 6 commits June 15, 2026 12:49
Streaming pricing strategy on Superfluid: backers stream a monthly ETHx rate
tagged to a target Markee; the Markee with the highest aggregate active stream
rate holds #1, and only while it is maintained. Every non-#1 Markee's GDA pool
is refunded at its aggregate inflow (losers net ~zero), 62% of the #1's
aggregate streams to the beneficiary, and 38% accrues for per-funder RevNet
settlement.

Contracts (contracts/streaming/):
- StreamingLeaderboard.sol — the SuperApp (EIP-1167 clone, IPricingStrategy):
  O(1) promotions in the inflow callback, permissionless claimTop decay poke,
  jail-safe termination + syncMarkee, MasterChef-style settlement accumulator,
  deposit-funded outbound buffers, clone-safe reentrancy guard.
- StreamingLeaderboardFactory.sol — clone factory + SuperApp registrar; holds
  the 62/38 RevNet/fee config read by every clone.
- StreamingInterfaces.sol — clone surface the factory calls.

Tests (test/StreamingLeaderboard.t.sol): 28 Base-mainnet fork tests covering
ranking, auto-flip/decay, refunds, settlement, jail-safety/liquidation,
migration in/out, and deposit/buffer accounting (buffers proven funded by
deposits, never RevNet settlement money).

Foundry setup: forge-std submodule (v1.16.1), npm deps (Superfluid 1.x,
OpenZeppelin 5.6.1), remappings, base RPC endpoint, skip broken legacy v0.1.
… Markees

Require Markee(m).pricingStrategy() == address(this) so only Markees actually
re-pointed to this board (via the old leaderboard's migratePricingStrategy) can
be registered. Add a full cross-contract migrate-in handoff fork test (legacy
v1.3 Leaderboard -> streaming board, then stream into the migrated Markee) plus
a negative test for the guard; replace the prior fake-address migrate-in test.
… until overtaken

Migrated lump-sum (legacy) markees have no stream, so they ranked below any
streaming markee the instant streaming launched. Give each a decaying
grandfather floor (effRate = max(aggregate stream rate, floor)) so an existing
message keeps its spot until a streaming message's monthly rate clears it.

- capture floor at migration in registerExistingMarkees from the markee's own
  totalFundsAdded: floorRate0 = total / (legacyFloorMonths * SECONDS_IN_MONTH),
  K default 3 (the "33% of total as a monthly rate" rule), read on-chain so it
  cannot be inflated; seed #1 to the highest-total migrated markee
- rank on _effRate = max(aggregateRate, _currentLegacyFloor); _currentLegacyFloor
  is full during a grace window, linear-decays to 0 over legacyFloorDecaySeconds,
  then 0 (pure math, jail-safe). topRate stays the real aggregate so a pre-paid
  legacy #1 opens no beneficiary stream; decay demotion reuses claimTop
- swap the two comparison sites (claimTop, _applyRankingWithCtx promotion) to
  _effRate(challenger) > _topThreshold(); clear floors on unregister
- admin setLegacyFloorConfig(months, grace, decay); public currentLegacyFloor /
  effectiveRate views so the frontend ranks by the number the contract enforces
- defaults K=3, grace 0, decay 0 (permanent until a decay window is configured)

3 fork tests added (hold/overtake, decay-then-claimTop, highest-total seeds top);
full suite 32/32 on the Base fork.
…settlement refactor

Move the streaming contracts under contracts/v1.3/streaming (with import fixups).

depositBuffer(address backer, uint256) now pulls from and credits an explicit
backer instead of msg.sender, so it can run inside host.batchCall: 301 forwarded
calls arrive with Superfluid's SimpleForwarder as msg.sender. The single-tx open
(wrap -> EIP-2612 permit -> depositBuffer -> createFlow) authorizes the buffer
allowance via permit because this Superfluid version has no ERC20-approve batch op.

Other streaming changes in this set:
- Markee: MessageChanged/NameChanged emit msg.sender instead of tx.origin.
- Replace markeeLeft with owner-gated transferMarkeeOwnership.
- Refactor RevNet settlement (_settleDirect/_settleViaRevNet, self-only
  _revNetPaySelf); add floorCaptured tracking.

Tests (Base fork, 42 passing) add the single-tx batchCall verification
(test_batchCall_singleTxOpensStream_fromNativeEth) and the caller!=backer deposit
path, alongside migration/settlement/ownership/pagination coverage.
…mPrice, totalLeaderboardFunds)

Expose the leaderboard-level read surface the frontend/API consumers expect,
so existing ABI consumers keep working against a StreamingLeaderboard without
changes:

- getTopMarkees(limit) -> (address[], uint256[]) ranked by live effective rate
  (max of aggregate stream rate and decaying grandfather floor). Same ABI shape
  as v1.3 Leaderboard.getTopMarkees; the second array is a wei/sec rate, not a
  cumulative fund total. Recomputes effRate live, so slot 0 is the true current
  #1 even while the enforced topMarkee lags in the decay/demotion direction
  before a claimTop poke.
- minimumPrice() aliases minimumMonthlyRate (the per-month participation floor).
- totalLeaderboardFunds() sums Markee.totalFundsAdded() (carried-over migrated
  history; 0 for natively-created streaming Markees).

Adds 5 Base-fork tests covering ranking order, the live-ranking-vs-stale-top
relationship and its reconciliation after claimTop, legacy-floor ranking, the
minimumPrice alias, and the totalLeaderboardFunds sum.
@vercel

vercel Bot commented Jun 19, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
markee Ready Ready Preview, Comment Jul 10, 2026 5:28am

@tnrdd tnrdd changed the title feat(streaming): v1.3-compat leaderboard reads (getTopMarkees, minimumPrice, totalLeaderboardFunds) feat: streaming pricing strategy Jun 19, 2026
tnrdd added 9 commits June 25, 2026 14:39
StreamingLeaderboard runtime bytecode was 29,877 bytes, over the 24,576
EIP-170 limit, so the factory constructor's `new StreamingLeaderboard()`
reverts with CreateContractSizeLimit on any real chain. The fork tests
missed it because Foundry's test EVM does not enforce the cap.

- Enable via_ir in foundry.toml (kept optimizer_runs=200 for runtime gas).
- Convert all 42 require(cond, "string") to 32 custom errors.
Result: 24,504 bytes, under the limit. 47/47 fork tests pass.

Update 3 test assertions from revert strings to error selectors.
Add script/DeployStreamingFactory.s.sol (forge deploy script); the
factory + implementation now deploy cleanly on a Base fork.

Also gitignore broadcast/ (forge run artifacts).
…Call

Add the streaming backer UI and its encoding, validated end-to-end on a
Base fork against a real deployed board.

- StreamModal (parameterized by board + markee): single-tx open via
  host.batchCall (wrap upgradeByETHTo / EIP-2612 permit / depositBuffer /
  CFA createFlow), plus stop and withdraw-deposit.
- lib/superfluid/streaming.ts: op-type constants, rate/buffer math, permit
  typed-data builder, buildOpenStreamOps, and the host/CFA-forwarder/ETHx
  ABIs. StreamingLeaderboardABI added to lib/contracts/abis.ts.
- scripts/open-stream-fork.mts: drives the real helper against an anvil
  fork; confirms the permit domain version "1" and the 4-op batch open a
  stream (adds tsx devDep).

Also fix a dangling V13_LEADERBOARDS import left by df0e08d (removed the
dead import in page.tsx; inlined the Cooperative leaderboard address in the
unused useMarkees hook) so the frontend type-checks again.
… StreamModal

- useStreamingMarkees: reads getTopMarkees (addresses + effectiveRates) plus
  per-Markee message/name/owner, returns board meta + ranked StreamingMarkee[]
  (filtered rate>0 to drop the seed Markee and unbacked messages), 30s polling
- streaming/[address] page: mirrors the superfluid board page but ranks/formats
  by ETH/month rate, wires StreamModal (back/manage) per row + the #1 spotlight,
  and adds a free createMarkee flow that chains into StreamModal via the
  MarkeeCreated receipt event so a fresh rate-0 message can be backed
- verify-streaming-read.mts: deterministic check of the read transform and
  formatRate round-trip (guard < 0.00005 so a 0.0001 ETH/mo stream that
  truncates on-chain still displays "0.0001 ETH/mo")

Verified: tsc --noEmit clean; getTopMarkees ranking fork tests pass on Base;
read-transform script green.
- STREAMING_FACTORY / STREAMING_ENABLED config from NEXT_PUBLIC_STREAMING_FACTORY
  so the streaming UI self-gates until the factory is deployed (or a fork address
  is set); production stays unaffected when unset
- useStreamingBoards: reads the factory registry (getLeaderboards) and each
  board's name + current #1 message/rate (getTopMarkees(1))
- /ecosystem/platforms/streaming: index of streaming boards (name, top message,
  top ETH/mo rate) linking to each board page, with a create CTA and a
  coming-soon empty state when disabled
- create-a-markee: gated "Streaming Board" platform reusing the shared
  createLeaderboard flow; activation links to the streaming board page
- raise-funding: gated streaming platform card for discoverability

tsc --noEmit clean.
- lib/streaming/keeper.ts: for each board from factory.getLeaderboards, claimTop when the
  live #1 (getTopMarkees[0]) has drifted from the enforced topMarkee, and settle backers
  (enumerated from BackerUpdated logs, filtered pendingSettlement>0, chunked). Plans only
  (dry run) when no wallet client is given
- app/api/cron/streaming-keeper: shared-secret-gated route (GET+POST) that signs with a
  gas-funded hot wallet; no-op when STREAMING_ENABLED is false. An automated job calls it
  on a schedule in production (README documents env + testing)
- scripts/keeper-logic-check.mts: deterministic mock-client check of the decision logic
- scripts/keeper-fork.mts: runnable dry-run/live keeper against a fork board

Verified: tsc --noEmit clean; decision-logic check green; the on-chain claimTop heal is
fork-proven by test_getTopMarkees_reflectsLiveRanking_beforeClaimTopHeals.
# Conflicts:
#	frontend/lib/contracts/addresses.ts
The JB terminal and RevNet project were redeployed (main: 9d8555d, 5ef86dc);
project 152 now reverts JBTerminalStore_RulesetPaymentPaused, reddening 8 tests.
Move to terminal 0x130f5Dd2 / project 7 to match frontend addresses.ts.

Pin the fork block so a future redeploy or ruleset change on live Base cannot
turn the suite red; FORK_BLOCK overrides it.
Mirrors the conversion already done in StreamingLeaderboard.sol. All 17 require
strings become 13 custom errors; runtime 5,286 -> 4,822 B, initcode 37,911 ->
37,065 B. No selector changes, so callers and the frontend ABI are unaffected.
@tnrdd
tnrdd marked this pull request as ready for review July 9, 2026 17:00
@tnrdd
tnrdd requested review from Corantin and pglavin2 July 9, 2026 17:01
The streaming frontend (StreamModal, keeper route, streaming board pages,
Superfluid encoding helpers, fork scripts) landed independently on
feat/streaming-frontend (#9), which supersedes the earlier draft carried
here: permit replaced by an approve tx, the standalone
/ecosystem/platforms/streaming pages unified under /markee/[address], and
the dev/testnet scripts kept out of the repo.

Leaves this PR contracts-only: StreamingLeaderboard + factory, deploy
script, Base-fork tests, and the Foundry toolchain.
Both PRs add a root .gitignore, so identical content lets the second one
merge without a both-added conflict. Adopts the frontend PR's anchored
Foundry patterns (/out/, /cache/, /broadcast/) plus .vercel and
/frontend/scripts/, and drops the bare CLAUDE.md rule that would have
ignored the tracked frontend/CLAUDE.md. lib/ stays unignored for the
forge-std submodule.

@gaston-review gaston-review 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.

Gaston Review

Verdict: Approved

Score: ███████░░░ 7/10

Pull Request Summary

This PR introduces a Superfluid-based streaming pricing strategy for the Markee leaderboard. Instead of lump-sum payments, backers open CFA streams into a StreamingLeaderboard contract. The highest-aggregate-rate Markee holds #1 (62% to beneficiary, 38% accrues for per-backer RevNet settlement via a MasterChef accumulator). Losers are refunded via GDA pools at their aggregate rate (net ~zero). There's a legacy grandfather floor system so migrated lump-sum Markees hold #1 until a stream overtakes their decaying floor. The whole thing is deployed as EIP-1167 clones from a factory that handles SuperApp registration and global RevNet/fee config. Also includes a Markee.sol fix replacing tx.origin with msg.sender in events, a comprehensive fork test suite against Base mainnet, a deploy script, and Foundry build config.

Review Summary

🔵 Suggestion — minimumMonthlyRate not enforced on flow updates
onFlowCreated enforces rate * SECONDS_IN_MONTH >= minimumMonthlyRate, but onFlowUpdated does not. A backer can lower their rate below the minimum after creation. If this is intentional (allowing graceful wind-down without forcing an all-or-nothing close), the asymmetry deserves a comment. If it's an oversight, a sub-minimum update could let dust streams linger on the leaderboard indefinitely.

⚪ Nitpick — _clone helper duplicated between StreamingLeaderboard and StreamingLeaderboardFactory
The EIP-1167 _clone function is copy-pasted verbatim in both contracts. Not a blocker (each contract needs its own copy since clones can't share internal functions from a different contract), but worth noting if a shared library is ever introduced.

🔵 Suggestion — Contract pushing EIP-170 size limit
The via_ir = true in foundry.toml is needed to keep StreamingLeaderboard under the 24,576-byte deployment limit. This leaves very little room for future additions. Any new feature or fix will need to watch bytecode size carefully — consider whether some view functions (getTopMarkees, totalLeaderboardFunds) could be moved to a separate reader contract to free up space.

This is a well-crafted, complex piece of DeFi infrastructure. The Superfluid integration is thorough — jail-safety is carefully handled in termination callbacks, the MasterChef settlement accumulator is correctly implemented, and the deposit-based buffer collateral model is sound. The fork test suite is excellent: it covers auto-flip promotions, decay-driven claimTop pokes, liquidation scenarios, migration in/out, batch settlement with reverting recipients, and buffer solvency reconciliation. I have a couple of minor findings but nothing that blocks shipping.

📌 4 inline comments
🔵 Suggestion: 2 · ⚪ Nitpick: 2


🔍 Reviewed by Gaston

Comment thread contracts/v1.3/streaming/StreamingLeaderboard.sol
Comment thread contracts/v1.3/Markee.sol
Comment thread contracts/v1.3/streaming/StreamingLeaderboard.sol
Comment thread contracts/v1.3/streaming/StreamingLeaderboard.sol
@Corantin
Corantin changed the base branch from main to dev July 9, 2026 19:56
- enforce minimumMonthlyRate on flow updates, not just creates
- withdrawDeposit reclaims the surplus above rate*BUFFER_PERIOD while
  streaming, so third-party depositBuffer inflation can't lock funds
- lock direct-deployed implementations (constructors set initialized)
- document BUFFER_PERIOD governance-config drift and the stale-refundRate
  defense-in-depth in _setRefundWithCtx
- privatize PRECISION/NATIVE_TOKEN getters to stay under EIP-170
  (24,532 B runtime, +44 margin)
- tests: disconnected loser liquidation, platform-fee settle path incl.
  10000 bps edge, min-rate update gate, surplus withdrawal while
  streaming, implementation locks, fuzzed open/close lifecycle
  invariants; fix stale RevNet project comment (53/53 on Base fork)

@gaston-review gaston-review 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.

Gaston Review

Verdict: Approved

Score: ████████░░ 8/10

Pull Request Summary

This PR introduces a Superfluid-based streaming pricing strategy for the Markee leaderboard. Backers open CFA streams into a StreamingLeaderboard SuperApp; the highest-aggregate-rate Markee holds #1 (62% forwarded to beneficiary, 38% accrues for per-backer RevNet settlement via a MasterChef accumulator). Losers are refunded via GDA pools. A legacy grandfather floor system protects migrated lump-sum Markees until stream rates overtake their decaying floor. The whole thing deploys as EIP-1167 clones from a factory that handles SuperApp registration and global RevNet/fee config. The latest push applies review fixes: minimumMonthlyRate enforcement on flow updates, implementation constructor locks, deposit surplus withdrawal, and comprehensive regression tests.

Review Summary

🔵 Suggestion — Aggregated platform fee pay is not try/catch-isolated
In _routeRevNetSettlement, per-backer RevNet pays are individually try/catch-isolated (a reverting recipient only restores its own claimable). But the final aggregated _revNetPay(terminal, projectId, feeReceiver, totalFee) is a bare call — if it reverts, the entire settle transaction reverts, undoing all the successful per-backer pays too. In practice this is unlikely (same terminal/project as the successful backer pays, and the fee receiver is governance-set), and the admin can recover by zeroing platformFeeReceiver. Still, wrapping the fee pay in a try/catch with a fallback (e.g., hold the fee as contract ETHx for a later admin sweep) would make settlement fully brick-proof.

The review-fix push cleanly addresses the prior round's suggestions — minimumMonthlyRate is now enforced symmetrically on creates and updates, both implementations are constructor-locked against hijacking, and deposit surplus is withdrawable while streaming. The regression test coverage is excellent, with targeted tests for every fix. The one minor concern I have is the un-isolated aggregated fee pay in settlement, but it's mitigated by admin control over the fee receiver. Production-ready.

📌 3 inline comments
🔵 Suggestion: 1 · ⚪ Nitpick: 2


🔍 Reviewed by Gaston

Comment thread contracts/v1.3/streaming/StreamingLeaderboard.sol
Comment thread contracts/v1.3/streaming/StreamingLeaderboardFactory.sol
Comment thread contracts/v1.3/streaming/StreamingLeaderboard.sol Outdated
- wrap the aggregated fee _revNetPay in the same self-call try/catch as
  the per-backer pays: a failed fee pay re-wraps to ETHx and credits the
  fee receiver's claimable (recoverable via settle) instead of reverting
  the whole batch
- extract _rewrapAndCredit, the recovery pair shared by all three settle
  paths
- privatize SECONDS_IN_MONTH/BASIS_POINTS_DIVISOR getters to stay under
  EIP-170 (24,521 B runtime, +55 margin)
- test: SelectiveRevertTerminal exercises the fee-pay failure and the
  post-recovery settle (54/54 on Base fork)

@gaston-review gaston-review 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.

Gaston Review

Verdict: Approved

Score: ████████░░ 8/10

Pull Request Summary

This PR introduces a Superfluid-based streaming pricing strategy for the Markee leaderboard. The latest push (2be5d72) addresses the one remaining suggestion from my prior review: the aggregated platform-fee pay in settle is now wrapped in try/catch with _rewrapAndCredit fallback, so a reverting fee receiver never bricks the settlement batch. The fee is credited as claimable[feeReceiver] and recoverable via a later settle([feeReceiver]) once the terminal is healthy.

All prior review items are now addressed. The fee isolation fix is clean — same pattern as the per-backer isolation, same _rewrapAndCredit fallback, same self-call try/catch frame. The test coverage for the new path (test_settle_feePayFailure_doesNotBrickBatch) verifies both the fallback credit and the recovery settle. The minor quirk that recovering the fee receiver's credit runs through _settleViaRevNet again (taking a fee-on-fee) is harmless — both portions route to the same feeReceiver via RevNet, so total value is preserved. This is production-ready.


🔍 Reviewed by Gaston

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.

2 participants