feat(events): decouple management authority from funder via per-event manager#23
feat(events): decouple management authority from funder via per-event manager#230xdevcollins wants to merge 2 commits into
Conversation
… manager Add an optional per-event manager (the address that authorizes select_winners + cancel) so the funding source (owner) can differ from the operating identity. An org can fund from any wallet, a treasury wallet, or an external/burner key while keeping management bound to its canonical wallet, so any privileged member can operate the event. - New DataKey::EventManager(u64) side-map. No EventRecord layout change, so no migration; legacy events fall back to owner-managed. - create_event records an optional manager; select_winners + all three cancel phases now require resolve_manager(...).require_auth() instead of event.owner.require_auth(). - New set_manager (gated by the current manager, so an org can rotate its operating wallet) + get_manager reads. - CreateEventParams gains manager: Option<Address>. - Tests: default-falls-back-to-owner, override-recorded, rotation, and a managed event selecting winners. Snapshots regenerated. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
📝 WalkthroughWalkthroughPer-event manager support was added to the events contract. Event creation can store a manager override, new APIs read and update it, and cancel/winner-selection authorization now uses the resolved manager instead of always the owner. Tests and snapshots were updated for the new event parameter and authority flow. ChangesEvent manager override
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes Poem
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
|
There was a problem hiding this comment.
Actionable comments posted: 1
🧹 Nitpick comments (1)
contracts/events/src/tests/cross_contract.rs (1)
1210-1247: ⚡ Quick winStrengthen manager auth tests to assert the signer identity, not only success paths.
These tests currently pass under global auth mocking even if authority accidentally remains
owner. Please assert required auths (or negative path) so regressions in manager gating are caught.Suggested test hardening pattern
#[test] fn manager_override_can_select_winners() { let ctx = setup(); let manager = Address::generate(&ctx.env); let bounty_id = create_bounty_with_manager(&ctx, &manager); let op_apply = BytesN::random(&ctx.env); ctx.events.apply_to_bounty(&bounty_id, &ctx.applicant, &op_apply); let winners = soroban_sdk::vec![ ... ]; let op_select = BytesN::random(&ctx.env); ctx.events.select_winners(&bounty_id, &winners, &op_select); + let auths = ctx.env.auths(); + let manager_required = auths.iter().any(|(addr, _)| *addr == manager); + let owner_required = auths.iter().any(|(addr, _)| *addr == ctx.owner); + assert!(manager_required, "select_winners must require manager auth"); + assert!(!owner_required, "owner auth should not gate managed events"); }🤖 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/events/src/tests/cross_contract.rs` around lines 1210 - 1247, Update the manager auth tests to explicitly assert signer identity: in manager_can_be_rotated and manager_override_can_select_winners use Address::generate to create manager and manager2, call ctx.events.set_manager(&id, &manager2) then assert the old manager cannot perform manager-only actions (e.g., expect ctx.events.select_winners or whatever manager-only entrypoint to fail when invoked with the original manager) and that the new manager (manager2) can successfully perform the same action; use ctx.events.apply_to_bounty / ctx.events.select_winners / ctx.events.get_event to verify failure for the wrong signer and success for the correct signer so the test fails if manager gating reverts to owner or global auth mocking hides regressions.
🤖 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/events/src/storage.rs`:
- Around line 218-225: The EventManager override can silently expire because
only DataKey::EventManager(id) is refreshed in
get_event_manager/set_event_manager while normal event activity only refreshes
DataKey::Event(id); update the code so touch_event_persistent (and/or the event
accessors get_event/set_event) also refreshes the corresponding
DataKey::EventManager(id) TTL when an event is accessed/updated: when
touch_event_persistent(env, &DataKey::Event(id)) is called, check
env.storage().persistent().get(&DataKey::EventManager(id)) and call
touch_event_persistent(env, &DataKey::EventManager(id)) (or reuse the existing
touch logic) so resolve_manager continues to see the delegated manager while the
event is active. Ensure you reference and update get_event, set_event,
touch_event_persistent, and DataKey::EventManager accordingly.
---
Nitpick comments:
In `@contracts/events/src/tests/cross_contract.rs`:
- Around line 1210-1247: Update the manager auth tests to explicitly assert
signer identity: in manager_can_be_rotated and
manager_override_can_select_winners use Address::generate to create manager and
manager2, call ctx.events.set_manager(&id, &manager2) then assert the old
manager cannot perform manager-only actions (e.g., expect
ctx.events.select_winners or whatever manager-only entrypoint to fail when
invoked with the original manager) and that the new manager (manager2) can
successfully perform the same action; use ctx.events.apply_to_bounty /
ctx.events.select_winners / ctx.events.get_event to verify failure for the wrong
signer and success for the correct signer so the test fails if manager gating
reverts to owner or global auth mocking hides regressions.
🪄 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: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: 90fbb4b4-967e-4b4b-8298-1a7b01a5bd92
📒 Files selected for processing (75)
contracts/events/src/event_ops.rscontracts/events/src/lib.rscontracts/events/src/storage.rscontracts/events/src/tests/contributions.rscontracts/events/src/tests/cross_contract.rscontracts/events/src/tests/crowdfunding.rscontracts/events/src/types.rscontracts/events/test_snapshots/tests/admin/apply_upgrade_after_expiry_reverts.1.jsoncontracts/events/test_snapshots/tests/admin/apply_upgrade_before_timelock_reverts.1.jsoncontracts/events/test_snapshots/tests/admin/cancel_pending_upgrade_clears_proposal.1.jsoncontracts/events/test_snapshots/tests/admin/propose_upgrade_records_pending_and_emits.1.jsoncontracts/events/test_snapshots/tests/contributions/add_funds_paged_storage_round_trip.1.jsoncontracts/events/test_snapshots/tests/contributions/add_funds_to_cancelled_event_reverts.1.jsoncontracts/events/test_snapshots/tests/contributions/anyone_can_top_up_an_active_event.1.jsoncontracts/events/test_snapshots/tests/contributions/below_minimum_contribution_reverts.1.jsoncontracts/events/test_snapshots/tests/contributions/cancel_at_boundary_pays_partners_full_no_owner_residual.1.jsoncontracts/events/test_snapshots/tests/contributions/cancel_clears_contributor_amounts_so_replay_state_is_clean.1.jsoncontracts/events/test_snapshots/tests/contributions/cancel_with_no_contributors_refunds_owner_in_full.1.jsoncontracts/events/test_snapshots/tests/contributions/cancel_with_owner_top_up_keeps_owner_residual_correct.1.jsoncontracts/events/test_snapshots/tests/contributions/cancel_with_partner_pool_refunds_partners_then_owner_residual.1.jsoncontracts/events/test_snapshots/tests/contributions/multiple_top_ups_from_same_contributor_aggregate_and_dont_duplicate_list.1.jsoncontracts/events/test_snapshots/tests/contributions/owner_top_up_grows_escrow_without_recording_contribution_entry.1.jsoncontracts/events/test_snapshots/tests/contributions/paged_cancel_owner_only_settles_inside_start.1.jsoncontracts/events/test_snapshots/tests/contributions/paged_cancel_processes_in_batches.1.jsoncontracts/events/test_snapshots/tests/contributions/replayed_add_funds_reverts.1.jsoncontracts/events/test_snapshots/tests/contributions/zero_or_negative_contribution_reverts.1.jsoncontracts/events/test_snapshots/tests/cross_contract/add_funds_uses_event_override_not_global.1.jsoncontracts/events/test_snapshots/tests/cross_contract/apply_charges_credits_via_profile.1.jsoncontracts/events/test_snapshots/tests/cross_contract/bounty_submit_requires_prior_application.1.jsoncontracts/events/test_snapshots/tests/cross_contract/bounty_submit_succeeds_after_apply.1.jsoncontracts/events/test_snapshots/tests/cross_contract/cancel_after_select_winners_refunds_only_remaining.1.jsoncontracts/events/test_snapshots/tests/cross_contract/cancel_already_cancelled_reverts.1.jsoncontracts/events/test_snapshots/tests/cross_contract/cancel_refunds_remaining_escrow_to_owner.1.jsoncontracts/events/test_snapshots/tests/cross_contract/claim_milestone_final_milestone_marks_event_completed.1.jsoncontracts/events/test_snapshots/tests/cross_contract/claim_milestone_idempotent_per_recipient_and_milestone.1.jsoncontracts/events/test_snapshots/tests/cross_contract/claim_milestone_invalid_milestone_index_reverts.1.jsoncontracts/events/test_snapshots/tests/cross_contract/claim_milestone_pays_per_milestone_amount.1.jsoncontracts/events/test_snapshots/tests/cross_contract/claim_milestone_rejects_non_grant_events.1.jsoncontracts/events/test_snapshots/tests/cross_contract/create_event_charges_override_rate_when_provided.1.jsoncontracts/events/test_snapshots/tests/cross_contract/create_event_omitted_override_falls_back_to_global_default.1.jsoncontracts/events/test_snapshots/tests/cross_contract/create_event_with_waiver_charges_no_fee.1.jsoncontracts/events/test_snapshots/tests/cross_contract/duplicate_apply_reverts.1.jsoncontracts/events/test_snapshots/tests/cross_contract/grant_last_milestone_sweeps_rounding_residue.1.jsoncontracts/events/test_snapshots/tests/cross_contract/hackathon_submit_creates_anchor_without_prior_apply.1.jsoncontracts/events/test_snapshots/tests/cross_contract/insufficient_credits_reverts.1.jsoncontracts/events/test_snapshots/tests/cross_contract/manager_can_be_rotated.1.jsoncontracts/events/test_snapshots/tests/cross_contract/manager_defaults_to_owner_and_override_is_recorded.1.jsoncontracts/events/test_snapshots/tests/cross_contract/manager_override_can_select_winners.1.jsoncontracts/events/test_snapshots/tests/cross_contract/replayed_apply_reverts_idempotently.1.jsoncontracts/events/test_snapshots/tests/cross_contract/resubmit_preserves_original_submitted_at_and_updates_uri.1.jsoncontracts/events/test_snapshots/tests/cross_contract/select_winners_handles_multi_recipient_distribution.1.jsoncontracts/events/test_snapshots/tests/cross_contract/select_winners_pays_against_remaining_escrow_including_top_ups.1.jsoncontracts/events/test_snapshots/tests/cross_contract/select_winners_pays_recipient_and_bumps_profile.1.jsoncontracts/events/test_snapshots/tests/cross_contract/select_winners_rejects_duplicate_position.1.jsoncontracts/events/test_snapshots/tests/cross_contract/select_winners_rejects_second_call_winners_already_selected.1.jsoncontracts/events/test_snapshots/tests/cross_contract/select_winners_replayed_reverts.1.jsoncontracts/events/test_snapshots/tests/cross_contract/select_winners_requires_position_in_distribution.1.jsoncontracts/events/test_snapshots/tests/cross_contract/submit_replayed_reverts.1.jsoncontracts/events/test_snapshots/tests/cross_contract/withdraw_refunds_half_credits.1.jsoncontracts/events/test_snapshots/tests/cross_contract/withdraw_submission_removes_anchor.1.jsoncontracts/events/test_snapshots/tests/cross_contract/withdraw_submission_without_submission_reverts.1.jsoncontracts/events/test_snapshots/tests/crowdfunding/builder_top_up_does_not_appear_in_contributor_list.1.jsoncontracts/events/test_snapshots/tests/crowdfunding/cancel_after_partial_claim_pro_rates_remaining.1.jsoncontracts/events/test_snapshots/tests/crowdfunding/cancel_refunds_all_partners_no_owner_residual.1.jsoncontracts/events/test_snapshots/tests/crowdfunding/cancel_with_no_contributions_just_marks_cancelled.1.jsoncontracts/events/test_snapshots/tests/crowdfunding/claim_milestone_last_drains_dust_remainder.1.jsoncontracts/events/test_snapshots/tests/crowdfunding/claim_milestone_out_of_range_reverts.1.jsoncontracts/events/test_snapshots/tests/crowdfunding/claim_milestone_replay_reverts.1.jsoncontracts/events/test_snapshots/tests/crowdfunding/claim_milestone_splits_evenly_across_remaining.1.jsoncontracts/events/test_snapshots/tests/crowdfunding/claim_milestone_with_empty_escrow_reverts.1.jsoncontracts/events/test_snapshots/tests/crowdfunding/community_top_ups_raise_escrow_from_zero.1.jsoncontracts/events/test_snapshots/tests/crowdfunding/create_with_zero_owner_deposit_and_auto_registered_winner.1.jsoncontracts/events/test_snapshots/tests/crowdfunding/crowdfunding_claim_milestone_requires_admin_auth.1.jsoncontracts/events/test_snapshots/tests/crowdfunding/select_winners_on_crowdfunding_reverts.1.jsoncontracts/events/test_snapshots/tests/crowdfunding/submit_on_crowdfunding_reverts.1.json
| pub fn get_event_manager(env: &Env, id: u64) -> Option<Address> { | ||
| let key = DataKey::EventManager(id); | ||
| let m: Option<Address> = env.storage().persistent().get(&key); | ||
| if m.is_some() { | ||
| touch_event_persistent(env, &key); | ||
| } | ||
| m | ||
| } |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Verify EventManager TTL refresh points and authority fallback path.
rg -n "EventManager|get_event_manager|set_event_manager|touch_event_persistent|resolve_manager|get_event\\(|set_event\\(" \
contracts/events/src/storage.rs contracts/events/src/event_ops.rsRepository: boundlessfi/boundless-contract
Length of output: 7208
🏁 Script executed:
#!/bin/bash
set -euo pipefail
sed -n '1,120p' contracts/events/src/storage.rs
echo "----"
sed -n '180,260p' contracts/events/src/storage.rs
echo "----"
sed -n '40,110p' contracts/events/src/event_ops.rs
echo "----"
sed -n '140,220p' contracts/events/src/event_ops.rs
echo "----"
sed -n '300,360p' contracts/events/src/event_ops.rs
echo "----"
sed -n '780,860p' contracts/events/src/event_ops.rsRepository: boundlessfi/boundless-contract
Length of output: 17659
🏁 Script executed:
#!/bin/bash
set -euo pipefail
rg -n "resolve_manager\\(" contracts/events/src/event_ops.rs
echo "----"
rg -n "get_event_manager\\(|set_event_manager\\(" contracts/events/src
echo "----"
rg -n "storage::get_event\\(|storage::set_event\\(" contracts/events/src/event_ops.rsRepository: boundlessfi/boundless-contract
Length of output: 2942
🏁 Script executed:
#!/bin/bash
set -euo pipefail
sed -n '280,360p' contracts/events/src/event_ops.rs
echo "----"
sed -n '380,440p' contracts/events/src/event_ops.rs
echo "----"
sed -n '440,520p' contracts/events/src/event_ops.rs
echo "----"
sed -n '600,660p' contracts/events/src/event_ops.rsRepository: boundlessfi/boundless-contract
Length of output: 10842
Manager override can expire independently and silently revert authority to owner
DataKey::EventManager(event_id) TTL is only refreshed in get_event_manager/set_event_manager, while normal event activity refreshes only DataKey::Event(event_id) via get_event/set_event. If the manager entry expires while the event stays alive, resolve_manager() falls back to event.owner, so subsequent require_auth in cancel/winner flows will accept the owner instead of the delegated manager.
Suggested fix
pub fn get_event(env: &Env, id: u64) -> Option<EventRecord> {
let key = DataKey::Event(id);
let rec: Option<EventRecord> = env.storage().persistent().get(&key);
if rec.is_some() {
touch_event_persistent(env, &key);
+ // Keep side-map authority key alive whenever the event itself is alive.
+ let _ = get_event_manager(env, id);
}
rec
}
pub fn set_event(env: &Env, id: u64, record: &EventRecord) {
let key = DataKey::Event(id);
env.storage().persistent().set(&key, record);
touch_event_persistent(env, &key);
+ // Keep side-map authority key aligned with event TTL if present.
+ let _ = get_event_manager(env, id);
}🤖 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/events/src/storage.rs` around lines 218 - 225, The EventManager
override can silently expire because only DataKey::EventManager(id) is refreshed
in get_event_manager/set_event_manager while normal event activity only
refreshes DataKey::Event(id); update the code so touch_event_persistent (and/or
the event accessors get_event/set_event) also refreshes the corresponding
DataKey::EventManager(id) TTL when an event is accessed/updated: when
touch_event_persistent(env, &DataKey::Event(id)) is called, check
env.storage().persistent().get(&DataKey::EventManager(id)) and call
touch_event_persistent(env, &DataKey::EventManager(id)) (or reuse the existing
touch logic) so resolve_manager continues to see the delegated manager while the
event is active. Ensure you reference and update get_event, set_event,
touch_event_persistent, and DataKey::EventManager accordingly.
There was a problem hiding this comment.
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (2)
deploy_and_upgrade.sh (2)
37-40:⚠️ Potential issue | 🟠 Major | ⚡ Quick winFix positional-arg parsing before using
NETWORKfor feature gating.
NETWORKis read from$4, but for non-propose-upgradeactions the usage places network in arg 3. That makes network selection (and the new testnet feature branch) incorrect.Suggested patch
ACTION=${1:-"status"} CONTRACT_KIND=${2:-"events"} -NETWORK=${4:-"testnet"} -SOURCE_ACCOUNT=${5:-"alice"} + +if [ "$ACTION" = "propose-upgrade" ]; then + NETWORK=${4:-"testnet"} + SOURCE_ACCOUNT=${5:-"alice"} +else + NETWORK=${3:-"testnet"} + SOURCE_ACCOUNT=${4:-"alice"} +fiAlso applies to: 73-77
149-151:⚠️ Potential issue | 🟡 Minor | ⚡ Quick winTimelock status output is inaccurate for
profileontestnet.For testnet profile builds (
--features testnet), upgrade timelock is 0, but the script always prints ~1 day.Suggested patch
- echo "The timelock is ~1 day at 5s/ledger (17_280 ledgers)." + if [ "$CONTRACT_KIND" = "profile" ] && [ "$NETWORK" = "testnet" ]; then + echo "Timelock is 0 ledgers for testnet profile builds (--features testnet)." + else + echo "The timelock is ~1 day at 5s/ledger (17_280 ledgers)." + fi🤖 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 `@deploy_and_upgrade.sh` around lines 149 - 151, The script always prints "The timelock is ~1 day at 5s/ledger (17_280 ledgers)." even when building for testnet where the timelock is 0; update deploy_and_upgrade.sh to detect the testnet profile (e.g., check the PROFILE variable or FEATURES/testnet flag used by the script) and conditionally print the appropriate message: if profile == "testnet" emit a line saying the timelock is 0 (or omit the ~1 day lines), otherwise keep the existing "~1 day" echo lines; locate the three echo statements ("The timelock is ~1 day...", "Run 'apply-upgrade'...", "Run 'status'...") and wrap them in the profile conditional so testnet output reflects timelock=0.
🤖 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.
Outside diff comments:
In `@deploy_and_upgrade.sh`:
- Around line 149-151: The script always prints "The timelock is ~1 day at
5s/ledger (17_280 ledgers)." even when building for testnet where the timelock
is 0; update deploy_and_upgrade.sh to detect the testnet profile (e.g., check
the PROFILE variable or FEATURES/testnet flag used by the script) and
conditionally print the appropriate message: if profile == "testnet" emit a line
saying the timelock is 0 (or omit the ~1 day lines), otherwise keep the existing
"~1 day" echo lines; locate the three echo statements ("The timelock is ~1
day...", "Run 'apply-upgrade'...", "Run 'status'...") and wrap them in the
profile conditional so testnet output reflects timelock=0.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: f3d8852f-bfe3-4d19-997e-75502e8887da
📒 Files selected for processing (85)
contracts/events/Cargo.tomlcontracts/events/src/admin.rscontracts/events/test_snapshots/tests/admin/apply_upgrade_after_expiry_reverts.1.jsoncontracts/events/test_snapshots/tests/admin/apply_upgrade_before_timelock_reverts.1.jsoncontracts/events/test_snapshots/tests/admin/cancel_pending_upgrade_clears_proposal.1.jsoncontracts/events/test_snapshots/tests/admin/propose_upgrade_records_pending_and_emits.1.jsoncontracts/events/test_snapshots/tests/contributions/add_funds_paged_storage_round_trip.1.jsoncontracts/events/test_snapshots/tests/contributions/add_funds_to_cancelled_event_reverts.1.jsoncontracts/events/test_snapshots/tests/contributions/anyone_can_top_up_an_active_event.1.jsoncontracts/events/test_snapshots/tests/contributions/below_minimum_contribution_reverts.1.jsoncontracts/events/test_snapshots/tests/contributions/cancel_at_boundary_pays_partners_full_no_owner_residual.1.jsoncontracts/events/test_snapshots/tests/contributions/cancel_clears_contributor_amounts_so_replay_state_is_clean.1.jsoncontracts/events/test_snapshots/tests/contributions/cancel_with_no_contributors_refunds_owner_in_full.1.jsoncontracts/events/test_snapshots/tests/contributions/cancel_with_owner_top_up_keeps_owner_residual_correct.1.jsoncontracts/events/test_snapshots/tests/contributions/cancel_with_partner_pool_refunds_partners_then_owner_residual.1.jsoncontracts/events/test_snapshots/tests/contributions/multiple_top_ups_from_same_contributor_aggregate_and_dont_duplicate_list.1.jsoncontracts/events/test_snapshots/tests/contributions/owner_top_up_grows_escrow_without_recording_contribution_entry.1.jsoncontracts/events/test_snapshots/tests/contributions/paged_cancel_owner_only_settles_inside_start.1.jsoncontracts/events/test_snapshots/tests/contributions/paged_cancel_processes_in_batches.1.jsoncontracts/events/test_snapshots/tests/contributions/replayed_add_funds_reverts.1.jsoncontracts/events/test_snapshots/tests/contributions/zero_or_negative_contribution_reverts.1.jsoncontracts/events/test_snapshots/tests/cross_contract/add_funds_uses_event_override_not_global.1.jsoncontracts/events/test_snapshots/tests/cross_contract/apply_charges_credits_via_profile.1.jsoncontracts/events/test_snapshots/tests/cross_contract/bounty_submit_requires_prior_application.1.jsoncontracts/events/test_snapshots/tests/cross_contract/bounty_submit_succeeds_after_apply.1.jsoncontracts/events/test_snapshots/tests/cross_contract/cancel_after_select_winners_refunds_only_remaining.1.jsoncontracts/events/test_snapshots/tests/cross_contract/cancel_already_cancelled_reverts.1.jsoncontracts/events/test_snapshots/tests/cross_contract/cancel_refunds_remaining_escrow_to_owner.1.jsoncontracts/events/test_snapshots/tests/cross_contract/claim_milestone_final_milestone_marks_event_completed.1.jsoncontracts/events/test_snapshots/tests/cross_contract/claim_milestone_idempotent_per_recipient_and_milestone.1.jsoncontracts/events/test_snapshots/tests/cross_contract/claim_milestone_invalid_milestone_index_reverts.1.jsoncontracts/events/test_snapshots/tests/cross_contract/claim_milestone_pays_per_milestone_amount.1.jsoncontracts/events/test_snapshots/tests/cross_contract/claim_milestone_rejects_non_grant_events.1.jsoncontracts/events/test_snapshots/tests/cross_contract/create_event_charges_override_rate_when_provided.1.jsoncontracts/events/test_snapshots/tests/cross_contract/create_event_omitted_override_falls_back_to_global_default.1.jsoncontracts/events/test_snapshots/tests/cross_contract/create_event_with_waiver_charges_no_fee.1.jsoncontracts/events/test_snapshots/tests/cross_contract/duplicate_apply_reverts.1.jsoncontracts/events/test_snapshots/tests/cross_contract/grant_last_milestone_sweeps_rounding_residue.1.jsoncontracts/events/test_snapshots/tests/cross_contract/hackathon_submit_creates_anchor_without_prior_apply.1.jsoncontracts/events/test_snapshots/tests/cross_contract/insufficient_credits_reverts.1.jsoncontracts/events/test_snapshots/tests/cross_contract/manager_can_be_rotated.1.jsoncontracts/events/test_snapshots/tests/cross_contract/manager_defaults_to_owner_and_override_is_recorded.1.jsoncontracts/events/test_snapshots/tests/cross_contract/manager_override_can_select_winners.1.jsoncontracts/events/test_snapshots/tests/cross_contract/replayed_apply_reverts_idempotently.1.jsoncontracts/events/test_snapshots/tests/cross_contract/resubmit_preserves_original_submitted_at_and_updates_uri.1.jsoncontracts/events/test_snapshots/tests/cross_contract/select_winners_handles_multi_recipient_distribution.1.jsoncontracts/events/test_snapshots/tests/cross_contract/select_winners_pays_against_remaining_escrow_including_top_ups.1.jsoncontracts/events/test_snapshots/tests/cross_contract/select_winners_pays_recipient_and_bumps_profile.1.jsoncontracts/events/test_snapshots/tests/cross_contract/select_winners_rejects_duplicate_position.1.jsoncontracts/events/test_snapshots/tests/cross_contract/select_winners_rejects_second_call_winners_already_selected.1.jsoncontracts/events/test_snapshots/tests/cross_contract/select_winners_replayed_reverts.1.jsoncontracts/events/test_snapshots/tests/cross_contract/select_winners_requires_position_in_distribution.1.jsoncontracts/events/test_snapshots/tests/cross_contract/submit_replayed_reverts.1.jsoncontracts/events/test_snapshots/tests/cross_contract/withdraw_refunds_half_credits.1.jsoncontracts/events/test_snapshots/tests/cross_contract/withdraw_submission_removes_anchor.1.jsoncontracts/events/test_snapshots/tests/cross_contract/withdraw_submission_without_submission_reverts.1.jsoncontracts/events/test_snapshots/tests/crowdfunding/builder_top_up_does_not_appear_in_contributor_list.1.jsoncontracts/events/test_snapshots/tests/crowdfunding/cancel_after_partial_claim_pro_rates_remaining.1.jsoncontracts/events/test_snapshots/tests/crowdfunding/cancel_refunds_all_partners_no_owner_residual.1.jsoncontracts/events/test_snapshots/tests/crowdfunding/cancel_with_no_contributions_just_marks_cancelled.1.jsoncontracts/events/test_snapshots/tests/crowdfunding/claim_milestone_last_drains_dust_remainder.1.jsoncontracts/events/test_snapshots/tests/crowdfunding/claim_milestone_out_of_range_reverts.1.jsoncontracts/events/test_snapshots/tests/crowdfunding/claim_milestone_replay_reverts.1.jsoncontracts/events/test_snapshots/tests/crowdfunding/claim_milestone_splits_evenly_across_remaining.1.jsoncontracts/events/test_snapshots/tests/crowdfunding/claim_milestone_with_empty_escrow_reverts.1.jsoncontracts/events/test_snapshots/tests/crowdfunding/community_top_ups_raise_escrow_from_zero.1.jsoncontracts/events/test_snapshots/tests/crowdfunding/create_with_zero_owner_deposit_and_auto_registered_winner.1.jsoncontracts/events/test_snapshots/tests/crowdfunding/crowdfunding_claim_milestone_requires_admin_auth.1.jsoncontracts/events/test_snapshots/tests/crowdfunding/select_winners_on_crowdfunding_reverts.1.jsoncontracts/events/test_snapshots/tests/crowdfunding/submit_on_crowdfunding_reverts.1.jsoncontracts/profile/Cargo.tomlcontracts/profile/src/admin.rscontracts/profile/src/credits.rscontracts/profile/src/lib.rscontracts/profile/src/tests/bootstrap.rscontracts/profile/src/tests/mod.rscontracts/profile/test_snapshots/tests/admin/apply_upgrade_after_expiry_reverts_profile.1.jsoncontracts/profile/test_snapshots/tests/admin/apply_upgrade_before_timelock_reverts_profile.1.jsoncontracts/profile/test_snapshots/tests/admin/propose_upgrade_records_pending.1.jsoncontracts/profile/test_snapshots/tests/bootstrap/bootstrap_self_creates_profile_for_caller.1.jsoncontracts/profile/test_snapshots/tests/bootstrap/bootstrap_self_demands_the_callers_own_auth_not_admin.1.jsoncontracts/profile/test_snapshots/tests/bootstrap/bootstrap_self_is_idempotent_for_existing_profile.1.jsoncontracts/profile/test_snapshots/tests/bootstrap/bootstrap_self_rejects_a_replayed_op_id.1.jsondeploy_and_upgrade.shscripts/deploy/deploy.sh
✅ Files skipped from review due to trivial changes (38)
- contracts/profile/test_snapshots/tests/bootstrap/bootstrap_self_rejects_a_replayed_op_id.1.json
- contracts/events/test_snapshots/tests/admin/propose_upgrade_records_pending_and_emits.1.json
- contracts/profile/test_snapshots/tests/bootstrap/bootstrap_self_is_idempotent_for_existing_profile.1.json
- contracts/profile/test_snapshots/tests/admin/apply_upgrade_before_timelock_reverts_profile.1.json
- contracts/events/test_snapshots/tests/admin/cancel_pending_upgrade_clears_proposal.1.json
- contracts/profile/test_snapshots/tests/admin/apply_upgrade_after_expiry_reverts_profile.1.json
- contracts/profile/test_snapshots/tests/admin/propose_upgrade_records_pending.1.json
- contracts/events/test_snapshots/tests/cross_contract/manager_defaults_to_owner_and_override_is_recorded.1.json
- contracts/events/test_snapshots/tests/admin/apply_upgrade_after_expiry_reverts.1.json
- contracts/events/test_snapshots/tests/cross_contract/withdraw_refunds_half_credits.1.json
- contracts/events/test_snapshots/tests/admin/apply_upgrade_before_timelock_reverts.1.json
- contracts/events/test_snapshots/tests/contributions/zero_or_negative_contribution_reverts.1.json
- contracts/profile/test_snapshots/tests/bootstrap/bootstrap_self_demands_the_callers_own_auth_not_admin.1.json
- contracts/events/test_snapshots/tests/cross_contract/select_winners_pays_recipient_and_bumps_profile.1.json
- contracts/events/test_snapshots/tests/crowdfunding/select_winners_on_crowdfunding_reverts.1.json
- contracts/events/test_snapshots/tests/cross_contract/apply_charges_credits_via_profile.1.json
- contracts/events/test_snapshots/tests/cross_contract/select_winners_requires_position_in_distribution.1.json
- contracts/events/test_snapshots/tests/cross_contract/select_winners_rejects_second_call_winners_already_selected.1.json
- contracts/events/test_snapshots/tests/contributions/paged_cancel_owner_only_settles_inside_start.1.json
- contracts/events/test_snapshots/tests/crowdfunding/cancel_refunds_all_partners_no_owner_residual.1.json
- contracts/events/test_snapshots/tests/contributions/cancel_clears_contributor_amounts_so_replay_state_is_clean.1.json
- contracts/events/test_snapshots/tests/contributions/replayed_add_funds_reverts.1.json
- contracts/events/test_snapshots/tests/cross_contract/add_funds_uses_event_override_not_global.1.json
- contracts/events/test_snapshots/tests/cross_contract/submit_replayed_reverts.1.json
- contracts/events/test_snapshots/tests/cross_contract/select_winners_handles_multi_recipient_distribution.1.json
- contracts/events/test_snapshots/tests/cross_contract/manager_override_can_select_winners.1.json
- contracts/events/test_snapshots/tests/crowdfunding/claim_milestone_splits_evenly_across_remaining.1.json
- contracts/events/test_snapshots/tests/cross_contract/withdraw_submission_removes_anchor.1.json
- contracts/events/test_snapshots/tests/contributions/multiple_top_ups_from_same_contributor_aggregate_and_dont_duplicate_list.1.json
- contracts/events/test_snapshots/tests/contributions/cancel_with_owner_top_up_keeps_owner_residual_correct.1.json
- contracts/events/test_snapshots/tests/cross_contract/bounty_submit_succeeds_after_apply.1.json
- contracts/events/test_snapshots/tests/crowdfunding/cancel_with_no_contributions_just_marks_cancelled.1.json
- contracts/events/test_snapshots/tests/contributions/below_minimum_contribution_reverts.1.json
- contracts/events/test_snapshots/tests/cross_contract/claim_milestone_final_milestone_marks_event_completed.1.json
- contracts/events/test_snapshots/tests/contributions/cancel_with_partner_pool_refunds_partners_then_owner_residual.1.json
- contracts/events/test_snapshots/tests/crowdfunding/claim_milestone_out_of_range_reverts.1.json
- contracts/events/test_snapshots/tests/cross_contract/duplicate_apply_reverts.1.json
- contracts/events/test_snapshots/tests/contributions/owner_top_up_grows_escrow_without_recording_contribution_entry.1.json
🚧 Files skipped from review as they are similar to previous changes (33)
- contracts/events/test_snapshots/tests/cross_contract/select_winners_rejects_duplicate_position.1.json
- contracts/events/test_snapshots/tests/cross_contract/create_event_with_waiver_charges_no_fee.1.json
- contracts/events/test_snapshots/tests/cross_contract/replayed_apply_reverts_idempotently.1.json
- contracts/events/test_snapshots/tests/cross_contract/cancel_already_cancelled_reverts.1.json
- contracts/events/test_snapshots/tests/crowdfunding/create_with_zero_owner_deposit_and_auto_registered_winner.1.json
- contracts/events/test_snapshots/tests/cross_contract/withdraw_submission_without_submission_reverts.1.json
- contracts/events/test_snapshots/tests/crowdfunding/claim_milestone_with_empty_escrow_reverts.1.json
- contracts/events/test_snapshots/tests/cross_contract/manager_can_be_rotated.1.json
- contracts/events/test_snapshots/tests/crowdfunding/submit_on_crowdfunding_reverts.1.json
- contracts/events/test_snapshots/tests/crowdfunding/crowdfunding_claim_milestone_requires_admin_auth.1.json
- contracts/events/test_snapshots/tests/contributions/add_funds_to_cancelled_event_reverts.1.json
- contracts/events/test_snapshots/tests/cross_contract/cancel_refunds_remaining_escrow_to_owner.1.json
- contracts/events/test_snapshots/tests/cross_contract/resubmit_preserves_original_submitted_at_and_updates_uri.1.json
- contracts/events/test_snapshots/tests/crowdfunding/builder_top_up_does_not_appear_in_contributor_list.1.json
- contracts/events/test_snapshots/tests/cross_contract/bounty_submit_requires_prior_application.1.json
- contracts/events/test_snapshots/tests/contributions/cancel_with_no_contributors_refunds_owner_in_full.1.json
- contracts/events/test_snapshots/tests/cross_contract/create_event_charges_override_rate_when_provided.1.json
- contracts/events/test_snapshots/tests/cross_contract/create_event_omitted_override_falls_back_to_global_default.1.json
- contracts/events/test_snapshots/tests/contributions/add_funds_paged_storage_round_trip.1.json
- contracts/events/test_snapshots/tests/cross_contract/insufficient_credits_reverts.1.json
- contracts/events/test_snapshots/tests/contributions/paged_cancel_processes_in_batches.1.json
- contracts/events/test_snapshots/tests/cross_contract/hackathon_submit_creates_anchor_without_prior_apply.1.json
- contracts/events/test_snapshots/tests/crowdfunding/claim_milestone_replay_reverts.1.json
- contracts/events/test_snapshots/tests/cross_contract/claim_milestone_invalid_milestone_index_reverts.1.json
- contracts/events/test_snapshots/tests/contributions/anyone_can_top_up_an_active_event.1.json
- contracts/events/test_snapshots/tests/cross_contract/select_winners_replayed_reverts.1.json
- contracts/events/test_snapshots/tests/crowdfunding/community_top_ups_raise_escrow_from_zero.1.json
- contracts/events/test_snapshots/tests/cross_contract/grant_last_milestone_sweeps_rounding_residue.1.json
- contracts/events/test_snapshots/tests/crowdfunding/claim_milestone_last_drains_dust_remainder.1.json
- contracts/events/test_snapshots/tests/cross_contract/claim_milestone_idempotent_per_recipient_and_milestone.1.json
- contracts/events/test_snapshots/tests/cross_contract/select_winners_pays_against_remaining_escrow_including_top_ups.1.json
- contracts/events/test_snapshots/tests/cross_contract/cancel_after_select_winners_refunds_only_remaining.1.json
- contracts/events/test_snapshots/tests/contributions/cancel_at_boundary_pays_partners_full_no_owner_residual.1.json
What
Splits the events contract's single
owner(funder and management authority) into a funder (owner) and an optional per-eventmanager. When set, the manager authorizesselect_winners+cancel; otherwise it falls back to the owner, so legacy events are unchanged.Why
Previously the wallet that funded an event was the only one that could operate it: if a member funded a hackathon from a personal or burner wallet, another privileged org member could not publish winners or cancel, because the contract's
require_authwas bound to that wallet. Pointing the manager at the org's canonical wallet (signed custodially by the backend) lets any privileged member operate the event regardless of funding source.Changes
DataKey::EventManager(u64)side-map. NoEventRecordlayout change, so no migration; legacy events fall back to owner-managed.resolve_manager(event_id, owner)+ the four management auths (select_winners,start_cancel,process_cancel_batch,finalize_cancel) now use it.create_eventrecords an optional manager;CreateEventParams.manager: Option<Address>.set_manager(gated by the current manager, so it is rotatable) +get_manager.Tests
cargo test -p boundless-events-> 79 passed (76 existing unchanged + 3 new: default-falls-back-to-owner, override-recorded, rotation, managed select_winners). Release WASM builds clean.Deploy
Deployed fresh to testnet with a single-key admin at
CATWJ3QXKJBS6UM7WHHNOJCAHWKMAKKRH7E4B4BYHDZ7UNMB4HUPJT2Y(USDC registered, profile contract repointed, backendBOUNDLESS_EVENTS_CONTRACT_ADDRESSupdated). Storage is additive and legacy events stay owner-managed, so nomigratestep is required.🤖 Generated with Claude Code
Summary by CodeRabbit
New Features
Tests
Chores