feat: streaming pricing strategy#8
Conversation
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.
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
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.
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.
There was a problem hiding this comment.
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
- 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)
There was a problem hiding this comment.
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
- 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)
There was a problem hiding this comment.
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
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.