Skip to content

feat: multi-organizer revenue split and co-host wallet management - #131

Merged
DioChuks merged 4 commits into
BuidlZone-Labs:mainfrom
josephchimebuka:feat/multi-organizer-revenue-split
Jun 28, 2026
Merged

feat: multi-organizer revenue split and co-host wallet management#131
DioChuks merged 4 commits into
BuidlZone-Labs:mainfrom
josephchimebuka:feat/multi-organizer-revenue-split

Conversation

@josephchimebuka

@josephchimebuka josephchimebuka commented Jun 23, 2026

Copy link
Copy Markdown
Contributor

Linked issue

Closes #122

What this PR does

Events are no longer locked to a single organizer wallet. An event can now be created with a revenue split — up to five recipients, each with a basis-point allocation summing to 10000. After the platform fee is deducted, each recipient withdraws their own share independently from the payments contract; no single party can drain the others' funds. The primary organizer (split index 0) keeps full admin rights and can flag a compromised co-host wallet, which freezes that recipient's share in escrow until an admin resolves the dispute by either releasing it to the recipient or reassigning it to the primary organizer.

Change type

  • New contract entrypoint
  • Struct / storage change
  • Event / emission change
  • Cross-contract interface change

Storage impact

Field Before After Notes
CreateEventParams.revenue_splits / Event.revenue_splits N/A Vec<(Address, u32)> Empty = legacy single-organizer payout
DataKey::EventSplits(Symbol) (payments) N/A Vec<RevenueSplit> Immutable once set
DataKey::SplitSettlement(Symbol) (payments) N/A SplitSettlement Net-distributable snapshot, frozen at first withdrawal
DataKey::SplitWithdrawn(Symbol, Address) (payments) N/A i128 Per-recipient payout tracking
DataKey::SplitFlagged(Symbol, Address) (payments) N/A bool Escrow freeze for flagged co-hosts

Is this a breaking storage change?

  • No — additive only. Existing events have no split configured and keep the exact legacy withdrawal behaviour. The new revenue_splits field is set on every new CreateEventParams (empty Vec for the single-organizer case).

On-chain vs. off-chain behaviour

Claim Storage level Event level Notes
Platform fee deducted before splits Fee accumulated to platform revenue in ensure_split_settled; PlatformFeeCollected emitted once
Each recipient paid exactly their share SplitWithdrawn guards double-withdrawal; RevenueWithdrawn emitted per recipient
Flagged share held in escrow Funds stay in the contract; CohostFlagged / FlaggedShareResolved emitted
Splits immutable after creation n/a sync_revenue_splits rejects any re-set; no setter exists

Cross-contract impact

  • Yes — added PaymentsContract::sync_revenue_splits(event_contract, event_id, splits), called by the event contract during create_event.
    • Callers updated: event-contract (create_event now syncs the split; withdraw_split and flag_cohost proxy to payments).
    • No existing signature changed; this is purely additive.

Security checklist

  • New entrypoints are auth-gated: sync_revenue_splits requires the linked event contract; withdraw_split requires the recipient; flag_cohost requires the primary organizer; resolve_flagged_share requires the payments admin.
  • Withdrawals honour state transitions: completed events respect event_end_ledger + withdrawal_delay + admin_extension; cancelled events respect the dispute window and the time-based withdrawable ratio (mirrors withdraw).
  • Legacy withdraw / withdraw_token / withdraw_all_tokens / withdraw_revenue / release_if_expired are rejected for split events, preventing double payout.
  • Integer division dust is intentionally routed to the primary organizer so the full net is always distributed and never stranded.

Test coverage

New tests added (payments — revenue_split_test.rs):

Test name What it proves
test_sync_revenue_splits_stores_and_reads_back Config persists and reads back as (Address, u32)
test_sync_revenue_splits_rejects_bad_sum / _more_than_five / _duplicate_and_zero Validation of sum=10000, max 5, no dupes/zeros
test_revenue_splits_are_immutable_once_set Re-set is rejected
test_sync_revenue_splits_rejects_foreign_caller Only the event contract can configure
test_platform_fee_deducted_before_split_and_independent_withdrawals Fee first, then independent shares
test_withdraw_split_rejects_double_withdraw_and_non_recipient No double payout; strangers rejected
test_withdraw_split_respects_withdrawal_delay Escrow delay enforced
test_rounding_dust_accrues_to_primary_organizer Dust → primary; full net distributed
test_only_primary_can_flag_and_cannot_flag_self Flag authorization
test_flagged_cohost_share_held_in_escrow Flagged share frozen; others unaffected
test_resolve_flag_release_to_recipient_allows_withdrawal Dispute release path
test_resolve_flag_reassign_to_primary_pays_primary_and_blocks_recipient Dispute reassign path
test_resolve_requires_flagged_recipient Resolve only on flagged
test_legacy_withdraw_paths_rejected_for_split_events Legacy paths blocked
test_cancelled_split_event_distributes_only_withdrawable_ratio Cancellation ratio + refund remainder

New tests added (event — integration_tests.rs): test_event_split_end_to_end_distribution, test_event_rejects_invalid_split_configurations, test_event_flag_cohost_through_front_door.

Test count: 20 new, 18 updated (existing CreateEventParams literals), 180 total passing (event 77, payments 103). cargo fmt --check and cargo clippy -D warnings clean; cargo build --release succeeds.

Acceptance criteria sign-off

  • AC: Event creation accepts revenue_splits: Vec<(Address, u32)> summing to 10000 — validate_revenue_splits in event/lib.rs; test_event_split_end_to_end_distribution.
  • AC: Maximum 5 split recipients — rejected in both contracts; test_sync_revenue_splits_rejects_more_than_five.
  • AC: Each recipient can independently withdraw — withdraw_split; test_platform_fee_deducted_before_split_and_independent_withdrawals.
  • AC: Primary organizer (index 0) retains admin rights — index 0 must equal the organizer; flag_cohost is primary-only; event admin ops keyed off event.organizer.
  • AC: Splits immutable after publish — set only at creation, sync_revenue_splits rejects re-set; test_revenue_splits_are_immutable_once_set.
  • AC: Platform fee deducted first — ensure_split_settled deducts fee before computing shares; asserted in the fee test.
  • AC: Compromised co-host can be flagged; flagged share escrowed pending dispute — flag_cohost + resolve_flagged_share; flagging/escrow/resolution tests.

What this PR deliberately does NOT cover

  • Event-status completion is not auto-synced from the event contract to payments (a pre-existing gap); operators still set the payments-side status. Split settlement reuses the same status/timing rules as the existing withdraw.
  • Mutable/renegotiable splits are intentionally out of scope — splits are immutable by design per the spec.

Reviewer focus areas

  1. ensure_split_settled in payments/lib.rs — fee-first ordering and the full vs. partial (cancelled) accounting against validate_revenue_invariant.
  2. recipient_share — primary absorbs rounding dust; confirm sum of shares equals net.
  3. The ensure_no_splits guards on every legacy withdrawal path.

Made with Cursor

Summary by CodeRabbit

  • New Features
    • Added multi-recipient revenue split support for events: view configured splits, withdraw allocated shares, and primary-only co-host flagging.
    • Payments now syncs split configuration from the event contract and enables split settlement, split withdrawals, and flagged co-host resolution.
  • Bug Fixes
    • Improved split validation with explicit rejections for malformed configurations, incorrect basis-point totals, and invalid primary organizer setup.
    • Disabled legacy revenue withdrawal entrypoints for split-configured events.
  • Tests
    • Added extensive integration/contract tests for end-to-end distribution, invalid splits, access control, co-host flag/resolve flows, and cancelled-event behavior.

Replaces the single-organizer payout assumption with configurable revenue
splits and co-host escrow handling (closes BuidlZone-Labs#122).

- Event creation accepts revenue_splits: Vec<(Address, u32)> (basis points),
  validated to 1-5 recipients summing to 10000, no duplicates/zeros, with the
  primary organizer pinned at index 0. Splits are set once and immutable.
- The split is synced to the payments contract, which settles each event once
  (deducting the platform fee first) and lets every recipient withdraw their
  allocated share independently via withdraw_split. Rounding dust accrues to
  the primary organizer so the full net is always distributed.
- Primary organizer can flag a compromised co-host wallet; the flagged share is
  held in escrow and cannot be withdrawn. Admin resolves a dispute by either
  releasing the share to the recipient or reassigning it to the primary.
- Legacy single-organizer withdrawal paths are rejected for split events to
  prevent double payout.

Adds 17 payments unit tests and 3 event integration tests covering the happy
path, fee ordering, independent withdrawals, the delay/dispute windows, split
validation, immutability, flagging, and dispute resolution.

Co-authored-by: Cursor <cursoragent@cursor.com>
@DioChuks

Copy link
Copy Markdown
Contributor

@josephchimebuka pls kindly resolve merge conflict, so i can complete review

@DioChuks

Copy link
Copy Markdown
Contributor

@josephchimebuka hello???

Resolve conflicts in error enums and payments storage keys by keeping
both postponement and revenue-split additions with non-overlapping codes.
Add revenue_splits to anon-claims test fixtures from upstream.

Co-authored-by: Cursor <cursoragent@cursor.com>
@coderabbitai

coderabbitai Bot commented Jun 27, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 6f5318a9-c20d-4965-8e75-d7c0071eb81c

📥 Commits

Reviewing files that changed from the base of the PR and between 8363c3f and 3caecf2.

📒 Files selected for processing (4)
  • contracts/payments/src/events.rs
  • contracts/payments/src/lib.rs
  • contracts/payments/src/revenue_split_test.rs
  • contracts/payments/src/storage.rs
✅ Files skipped from review due to trivial changes (1)
  • contracts/payments/src/events.rs
🚧 Files skipped from review as they are similar to previous changes (3)
  • contracts/payments/src/storage.rs
  • contracts/payments/src/lib.rs
  • contracts/payments/src/revenue_split_test.rs

📝 Walkthrough

Walkthrough

Adds configurable revenue splits to event creation and settlement. The event contract validates and stores split data, syncs it to payments, and exposes split-aware actions. The payments contract adds split storage, settlement, withdrawals, flagging, and dispute resolution. Tests cover the new flows and legacy-path restrictions.

Changes

Multi-organizer Revenue Split Feature

Layer / File(s) Summary
Types, errors, and events
contracts/event/src/types.rs, contracts/event/src/errors.rs, contracts/payments/src/types.rs, contracts/payments/src/errors.rs, contracts/payments/src/events.rs
Event and CreateEventParams gain revenue_splits; payments adds RevenueSplit, SplitSettlement, and FlagResolution; new split-related error variants and contract events are added.
Payments split storage
contracts/payments/src/storage.rs
Split-related storage keys and helpers are added for configuration, settlement snapshots, withdrawn amounts, and flagged state.
Event split validation and proxying
contracts/event/src/lib.rs
Event creation validates and persists split configuration, syncs non-empty splits to payments, and adds split query/withdraw/flag proxy methods.
Payments split settlement and withdrawal
contracts/payments/src/lib.rs
Split settlement, legacy withdrawal blocking, recipient payout calculation, split configuration syncing, per-recipient withdrawal, and co-host dispute resolution are added.
Event and payments tests
contracts/event/src/integration_tests.rs, contracts/event/src/test.rs, contracts/event/src/test_anon_claims.rs, contracts/event/src/test_claims.rs, contracts/event/src/test_privacy.rs, contracts/payments/src/revenue_split_test.rs
New tests cover split creation, invalid configuration rejection, withdrawal flows, flagging and resolution, and existing event test setup updates for the new field.

Estimated code review effort

🎯 5 (Critical) | ⏱️ ~120 minutes

Suggested reviewers

  • DioChuks

Poem

I’m a rabbit with a ledger bright,
Splitting coins by basis points just right.
Co-hosts hop in, then shares unwind,
And flagged paws wait till peace is signed.
🎟️🐇

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed Clear, specific title matches the multi-organizer revenue split and co-host wallet management changes.
Description check ✅ Passed The description covers the required sections, acceptance criteria, storage impact, tests, and scope boundaries.
Linked Issues check ✅ Passed The changes satisfy #122: split configuration, five-recipient limit, independent withdrawals, admin rights, immutability, fee-first settlement, and co-host flagging.
Out of Scope Changes check ✅ Passed The diff appears focused on revenue splits and co-host management, with tests and plumbing that directly support #122.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Comment @coderabbitai help to get the list of available commands.

Resolve conflicts with postponement, anon-claims, and merged PR BuidlZone-Labs#132
(zkEmail commitments). Keep both revenue-split and commitment APIs with
non-overlapping PaymentError codes (35–42).

Co-authored-by: Cursor <cursoragent@cursor.com>

@coderabbitai coderabbitai 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.

Actionable comments posted: 4

🧹 Nitpick comments (2)
contracts/payments/src/lib.rs (1)

1721-1737: 🔒 Security & Privacy | 🔵 Trivial | ⚡ Quick win

Mirror the primary-organizer invariant in payments.

flag_cohost treats splits[0] as the admin, but sync_revenue_splits never checks that recipient against the stored event organizer. Since this contract holds the funds, validate index 0 before storing splits.

Proposed hardening
+        let config = storage::get_event_config(&env, &event_id)
+            .ok_or(PaymentError::InvalidOrganizer)?;
         let mut normalized: soroban_sdk::Vec<RevenueSplit> = soroban_sdk::Vec::new(&env);
         let mut total: u32 = 0;
         for i in 0..len {
             let (recipient, bps) = splits.get(i).ok_or(PaymentError::InvalidSplitConfig)?;
+            if i == 0 && recipient != config.organizer {
+                return Err(PaymentError::InvalidSplitConfig);
+            }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@contracts/payments/src/lib.rs` around lines 1721 - 1737, The revenue split
normalization in sync_revenue_splits currently validates duplicates and bps
totals but never enforces that splits[0] matches the stored event organizer,
which flag_cohost assumes as the admin. Update sync_revenue_splits to check the
first split’s recipient against the event organizer before pushing into
normalized, using the existing event lookup/state in
contracts/payments/src/lib.rs and preserving the current InvalidSplitConfig
error path on mismatch.
contracts/payments/src/revenue_split_test.rs (1)

523-567: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add the refund-before-split-withdraw cancellation case.

This test only covers split withdrawal before attendee refunds. Add the reverse order so the test proves the remaining organizer/co-host share is still withdrawable after claim_refund.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@contracts/payments/src/revenue_split_test.rs` around lines 523 - 567, The
cancellation coverage in
test_cancelled_split_event_distributes_only_withdrawable_ratio only verifies
withdrawals before attendee refunds; add the reverse order in the same test or a
companion test so it exercises `claim_refund` first and then `withdraw_split`.
Use the existing
`test_cancelled_split_event_distributes_only_withdrawable_ratio`,
`client.claim_refund`, and `client.withdraw_split` flow to confirm the
organizer/cohost share still remains withdrawable after refunds, and assert the
final balances reflect the same 50% distributable split.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@contracts/payments/src/events.rs`:
- Around line 276-303: The FlaggedShareResolved event currently uses two
different identifiers for the same event in the topic and payload, so make them
canonical and consistent. Update the FlaggedShareResolved contractevent topic
and the event_type field in emit_flagged_share_resolved/event_type(...) to use
the same identifier everywhere, and keep the struct, publisher, and consumers
aligned on that single name.

In `@contracts/payments/src/lib.rs`:
- Around line 377-386: The split settlement path in the payout flow is only
using currently held payments, so cancelled events can lose track of funds after
`claim_refund` marks payments `Refunded`. Update the logic around
`collect_held_payments_for_token` and the split-withdraw calculation to use a
cancellation snapshot or payment records that still account for partially
refunded cancellation claims, rather than only unreleased balances. Make sure
the settlement path for cancelled split events can still derive the
organizer/co-host withdrawable amount even after attendee refunds have been
claimed.
- Around line 400-419: The full-settlement path in the payments release flow
clears token revenue but leaves aggregate event revenue unchanged, so update the
withdrawable_ratio_bps == 10_000 branch in the settlement logic to also clear
EventRevenue. Use the same storage helpers already used in the partial branch,
and keep the change localized around the payment release loop and the
storage::set_event_token_revenue / storage::set_event_revenue calls.

In `@contracts/payments/src/storage.rs`:
- Around line 712-779: Refresh TTL on read for split-related storage helpers so
persisted state does not expire after long-lived inactivity. Update the read
paths in get_splits, get_split_settlement, get_split_withdrawn, and
is_split_flagged to extend the TTL for their corresponding DataKey entries,
using the same TTL_THRESHOLD and TTL_BUMP pattern already used in set_splits,
set_split_settlement, set_split_withdrawn, and set_split_flagged. Keep the
behavior localized to these helper functions so has_splits and ensure_no_splits
continue to work correctly for EventSplits and SplitFlagged state.

---

Nitpick comments:
In `@contracts/payments/src/lib.rs`:
- Around line 1721-1737: The revenue split normalization in sync_revenue_splits
currently validates duplicates and bps totals but never enforces that splits[0]
matches the stored event organizer, which flag_cohost assumes as the admin.
Update sync_revenue_splits to check the first split’s recipient against the
event organizer before pushing into normalized, using the existing event
lookup/state in contracts/payments/src/lib.rs and preserving the current
InvalidSplitConfig error path on mismatch.

In `@contracts/payments/src/revenue_split_test.rs`:
- Around line 523-567: The cancellation coverage in
test_cancelled_split_event_distributes_only_withdrawable_ratio only verifies
withdrawals before attendee refunds; add the reverse order in the same test or a
companion test so it exercises `claim_refund` first and then `withdraw_split`.
Use the existing
`test_cancelled_split_event_distributes_only_withdrawable_ratio`,
`client.claim_refund`, and `client.withdraw_split` flow to confirm the
organizer/cohost share still remains withdrawable after refunds, and assert the
final balances reflect the same 50% distributable split.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 6db5afdc-d39d-4cc1-86fa-71d07776c151

📥 Commits

Reviewing files that changed from the base of the PR and between a1b3246 and c1bb9c5.

📒 Files selected for processing (14)
  • contracts/event/src/errors.rs
  • contracts/event/src/integration_tests.rs
  • contracts/event/src/lib.rs
  • contracts/event/src/test.rs
  • contracts/event/src/test_anon_claims.rs
  • contracts/event/src/test_claims.rs
  • contracts/event/src/test_privacy.rs
  • contracts/event/src/types.rs
  • contracts/payments/src/errors.rs
  • contracts/payments/src/events.rs
  • contracts/payments/src/lib.rs
  • contracts/payments/src/revenue_split_test.rs
  • contracts/payments/src/storage.rs
  • contracts/payments/src/types.rs

Comment thread contracts/payments/src/events.rs Outdated
Comment thread contracts/payments/src/lib.rs
Comment thread contracts/payments/src/lib.rs
Comment thread contracts/payments/src/storage.rs

@coderabbitai coderabbitai 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.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@contracts/payments/src/errors.rs`:
- Around line 55-60: The PaymentError discriminants for CommitmentAlreadySet and
CommitmentNotAllowed must remain stable; do not renumber them when splitting the
new errors. Update the PaymentError enum in the errors definition so the new
commitment-related variants are appended after the existing 42 value, or
otherwise explicitly preserve the original numeric values, and verify any
match/serialization logic still maps the same variants to the same ABI codes.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: fef22867-5188-47b6-8a06-e1a7cd51be77

📥 Commits

Reviewing files that changed from the base of the PR and between c1bb9c5 and 8363c3f.

📒 Files selected for processing (4)
  • contracts/payments/src/errors.rs
  • contracts/payments/src/events.rs
  • contracts/payments/src/lib.rs
  • contracts/payments/src/types.rs
🚧 Files skipped from review as they are similar to previous changes (3)
  • contracts/payments/src/events.rs
  • contracts/payments/src/types.rs
  • contracts/payments/src/lib.rs

Comment thread contracts/payments/src/errors.rs
@DioChuks

Copy link
Copy Markdown
Contributor

@josephchimebuka pls resolve coderabbit requested changes, and then ensure CI Format check passes

…-Labs#122)

- Align FlaggedShareResolved event topic with payload name
- Settle cancelled split events from payment records after refunds
- Clear aggregate EventRevenue on full split settlement
- Refresh split storage TTL on reads
- Require split index 0 to match event organizer
- Add test for split withdrawal after attendee refund
- Run cargo fmt

Co-authored-by: Cursor <cursoragent@cursor.com>
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.

Implement multi-organizer revenue split and co-host wallet management

2 participants