Skip to content

fix(events): require acceptance for manager delegation (#70) - #88

Merged
0xdevcollins merged 2 commits into
boundlessfi:testnetfrom
Michaelkingsdev:fix/issue-70-manager-acceptance
Jul 20, 2026
Merged

fix(events): require acceptance for manager delegation (#70)#88
0xdevcollins merged 2 commits into
boundlessfi:testnetfrom
Michaelkingsdev:fix/issue-70-manager-acceptance

Conversation

@Michaelkingsdev

@Michaelkingsdev Michaelkingsdev commented Jul 19, 2026

Copy link
Copy Markdown
Contributor

Closes #70

Problem

create_event(params.manager) and set_manager granted privileged authority (select_winners, cancel, manager rotation) to an address that never authorized. A typo, wrong-network address, or a malicious frontend swapping params.manager at signing time could:

  • lock the owner out permanently — only the current manager can rotate the manager, so pointing it at a dead/unowned address freezes the event and its escrow; or
  • hand escrow-draining select_winners authority to an attacker with no further owner sign-off (non-crowdfunding pillars).

Root cause: transferring a privileged role to an address that never proved it controls the key.

Fix

Two-step propose/accept, mirroring the existing set_admin/accept_admin rotation:

  • propose_manager(event_id, new_manager) — current authority (the manager, or the owner when none is set) records a pending proposal with a short expiry. No authority transfers. Emits ManagerProposed.
  • accept_manager(event_id) — the proposed address must require_auth() to accept before the role transfers. Emits ManagerChanged (which also closes the missing set_manager storage-change event flagged by Scout).
  • cancel_pending_manager(event_id) — current authority vetoes a pending proposal.
  • create_event now only proposes params.manager; authority resolves to the owner (via resolve_manager) until acceptance, so the owner is never locked out.
  • get_pending_manager(event_id) read added.
  • set_manager removed (its instant-transfer semantics were the vulnerability).

The only path that grants manager authority (accept_manager) calls pending.target.require_auth() immediately before writing the manager, so a never-accepting address gains nothing and the owner is never displaced.

Storage

Extended append-only (no reorder): new DataKey::PendingManager(u64) and PendingManager { target, expires_at_ledger }.

Tests

Rewrote the manager suite in cross_contract.rs:

  • default-to-owner with no pending proposal
  • manager named at creation is only proposed, not granted
  • propose → accept transfers control
  • unaccepted proposal leaves the owner in authority (verified via env.auths())
  • accepted manager holds select_winners authority
  • rotation via propose/accept
  • cancel_pending_manager vetoes a proposal
  • accept with no proposal reverts
  • expired proposal cannot be accepted

Updated the one prior set_manager call in cancel_refund.rs, and the pause-guarded-function list in docs/mainnet-deploy-runbook.md.

Verification

  • cargo test --all — 269 passed (203 events + 66 profile)
  • cargo build --release --target wasm32v1-none — builds
  • cargo fmt --check and cargo clippy -p boundless-events — clean

Summary by CodeRabbit

  • New Features

    • Added a secure, two-step manager transfer process with proposal, acceptance, and cancellation.
    • Added expiration for pending manager changes.
    • Added visibility into pending manager proposals.
    • Added events for manager proposals, successful changes, and cancellations.
    • Initial event managers now require acceptance before becoming active.
  • Breaking Changes

    • Removed direct manager assignment in favor of the proposal and acceptance workflow.
  • Documentation

    • Updated deployment guidance to reflect the new manager controls and pause behavior.

Manager delegation previously granted privileged authority (select_winners,
cancel, manager rotation) to an address that never authorized. A typo,
wrong-network address, or a swapped params.manager at signing could lock the
owner out permanently, or hand escrow-draining select_winners authority to an
attacker with no further owner sign-off.

Replace the immediate assignment with a two-step propose/accept flow mirroring
the existing admin rotation:

- propose_manager: current authority (manager, else owner) records a pending
  proposal with a short expiry; no authority transfers.
- accept_manager: the proposed address must require_auth to accept before the
  role transfers.
- cancel_pending_manager: current authority vetoes a pending proposal.
- create_event now only proposes params.manager; the owner stays in control
  until acceptance.

Emit ManagerProposed and ManagerChanged (the latter also closes the missing
set_manager storage-change event). Remove set_manager.

Storage layout extended append-only (DataKey::PendingManager, PendingManager).
@coderabbitai

coderabbitai Bot commented Jul 19, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@0xdevcollins, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 23 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: d11e2d4e-a620-43ee-8536-dd1becf03797

📥 Commits

Reviewing files that changed from the base of the PR and between 48511ba and 37606da.

📒 Files selected for processing (8)
  • contracts/events/src/admin.rs
  • contracts/events/src/errors.rs
  • contracts/events/src/event_ops.rs
  • contracts/events/src/lib.rs
  • contracts/events/src/storage.rs
  • contracts/events/src/tests/cancel_refund.rs
  • contracts/events/src/tests/cross_contract.rs
  • contracts/events/src/types.rs
📝 Walkthrough

Walkthrough

The events contract replaces direct manager assignment with an expiring two-step proposal and acceptance workflow. It adds pending-manager storage, lifecycle events, cancellation, public APIs, authorization checks, expiry handling, tests, and updated pause documentation.

Changes

Manager rotation workflow

Layer / File(s) Summary
Pending manager contracts and storage
contracts/events/src/types.rs, contracts/events/src/errors.rs, contracts/events/src/events.rs, contracts/events/src/storage.rs
Adds the pending-manager data shape and storage key, mismatch error, lifecycle events, and persistent storage helpers.
Proposal and acceptance entry points
contracts/events/src/event_ops.rs, contracts/events/src/lib.rs
Replaces direct manager assignment with expiring propose, accept, cancel, and query APIs; event creation now creates a pending proposal.
Workflow validation and operational documentation
contracts/events/src/tests/*, docs/mainnet-deploy-runbook.md
Tests authorization, acceptance, rotation, cancellation, and expiry; documents the updated pause-guarded methods.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Caller
  participant EventContract
  participant Storage
  participant PendingManager
  Caller->>EventContract: propose_manager(event_id, target)
  EventContract->>Storage: store pending target and expiry
  Storage-->>EventContract: pending manager saved
  PendingManager->>EventContract: accept_manager(event_id)
  EventContract->>Storage: read pending manager
  EventContract->>Storage: activate target and clear pending state
  EventContract-->>PendingManager: ManagerChanged
Loading

Suggested reviewers: 0xdevcollins, chigozzdevv

Poem

A bunny proposed with a ledger-bound plan,
“Accept when you’re ready, dear manager hare.”
The old guard stays steady while offers await,
Expired hops vanish from pending state.
Events bloom softly—rotation is fair.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely describes the manager delegation acceptance flow changed in this PR.
Linked Issues check ✅ Passed The changes implement the required propose/accept flow, expiry, events, and tests, preserving old authority until acceptance.
Out of Scope Changes check ✅ Passed The updates stay within the manager delegation fix and its supporting tests and docs, with no unrelated changes apparent.
Docstring Coverage ✅ Passed Docstring coverage is 83.33% which is sufficient. The required threshold is 80.00%.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

@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/events/src/event_ops.rs`:
- Around line 216-219: Remove the storage::clear_pending_manager call from the
expiry branch in the pending-manager validation flow, since returning
Error::PendingManagerMismatch rolls back that write. Preserve the existing
expiry error behavior and avoid adding cleanup on the failing path.
🪄 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: c53ef2f6-33ea-4df0-a761-d3fe512bac4c

📥 Commits

Reviewing files that changed from the base of the PR and between 188bc71 and 48511ba.

📒 Files selected for processing (9)
  • contracts/events/src/errors.rs
  • contracts/events/src/event_ops.rs
  • contracts/events/src/events.rs
  • contracts/events/src/lib.rs
  • contracts/events/src/storage.rs
  • contracts/events/src/tests/cancel_refund.rs
  • contracts/events/src/tests/cross_contract.rs
  • contracts/events/src/types.rs
  • docs/mainnet-deploy-runbook.md

Comment thread contracts/events/src/event_ops.rs
…rror-enum cap

Brings the two-step manager delegation up to current testnet (boundlessfi#90 pull-model
claims, boundlessfi#84 snapshots) and makes it build and pass there.

Error enum: testnet is at the 50-case contracterror cap, so a new
PendingManagerMismatch variant would not compile. Instead generalize the
two admin-rotation variants — PendingAdminMismatch/PendingAdminExpired ->
PendingRotationMismatch/PendingRotationExpired (discriminants 12/13
unchanged, so no on-chain code change) — and share them across both
two-step rotations. accept_manager now distinguishes no-pending
(Mismatch) from expired (Expired), which is more precise than the
original single-variant usage.

Conflict resolution:
- types.rs / storage.rs / event_ops.rs: PendingManager keys and helpers
  ordered after the 1.3.0 prize keys; append-only, no reorder.
- cross_contract.rs: kept the new manager suite; the accepted-manager
  test now claims the prize before asserting Completed, since under the
  pull model select_winners records rather than pays.

Verified: cargo test 216 events + 66 profile green; make build OK
(events wasm 56,202 bytes, < 64 KB); fmt + clippy clean.
@almanax-ai

almanax-ai Bot commented Jul 20, 2026

Copy link
Copy Markdown

Quota reached

Your plan allows 300 CI/CD file units per month. You've used 280 and this scan would add 23 more (total: 303).

@0xdevcollins
0xdevcollins merged commit 246d307 into boundlessfi:testnet Jul 20, 2026
4 checks passed
0xdevcollins added a commit that referenced this pull request Jul 23, 2026
…n) (#97)

The queries merged in #80 return zero rows against Dune: they filtered on
JSON_EXTRACT_SCALAR(topics_decoded,'$[0]') (an ScVal object, never the
event name) and read data_decoded as flat $.field (it's a type-wrapped
ScVal map). Both silently yield null, so every dashboard panel is empty.

Verified the real stellar.history_contract_events shapes on live Dune data
and rewrote all 10 dune-queries/*.sql to:
- filter the event name via topics_decoded '$[0].symbol'
- decode fields by rebuilding data_decoded '$.map' into
  MAP(field -> ScVal JSON) with map_from_entries(), then reading each by
  ScVal type ($.u64, $.i128, $.address, $.string, $.vec[0].symbol)
- add the closed_at_date partition filter (avoids full-table scans)
- to_hex(transaction_hash) (it is varbinary)

Every primitive was executed against live Soroban events on Dune. The one
field that can't be checked without a real Boundless event — pillar's unit-
enum encoding — is emitted as pillar_raw in the decode test for confirmation.

Doc fixes: rewrote the §1 decoding reference; corrected the fee accounting
(escrow holds the full budget, fee charged on top; fee revenue is not in the
events); added the ManagerProposed/ManagerChanged/PendingManagerCancelled
events (#88); pointed §4 at the canonical .sql files instead of duplicating
now-corrected SQL inline.
0xdevcollins added a commit that referenced this pull request Jul 23, 2026
…ractmeta (#98)

Version stamps had drifted and were internally inconsistent: events
contractmeta said 1.2.0 while INITIAL_VERSION said 1.3.0, and neither
reflected the public-surface / storage changes merged since (#86 submission
cap, #88 manager two-step, #96 OpSeen namespacing). Profile was still 1.1.0
despite #95 namespacing its OpSeen.

Bump both contracts coherently — INITIAL_VERSION, contractmeta, and the
Cargo package version all set to:
  events  1.3.0 -> 1.4.0   (submission cap, manager two-step, OpSeen ns)
  profile 1.1.0 -> 1.2.0   (namespaced OpSeen)

version()-asserting admin tests updated to match. 225 events + 66 profile
tests green; make build OK (events 56,091 B, profile 15,893 B, both under
the 64 KB ceiling); fmt clean.
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.

Manager delegation has no acceptance step (event control can be locked/hijacked)

2 participants